Making A Mesh Deformation Ocean

If you stumbled upon this, more likely than not, you want to create a deformable mesh ocean like a lot of these cutting edge games nowadays. Good news is that after days of my own searching an learning to no avail, I eventually figured it out and now I’ll share what my process was.

Note: This tutorial only covers the setup, the mesh, and the basics to scripting the waves. If you want an in depth tutorial on the wave equation and scripting side of things, use this well written post.

This is my first tutorial


Ok,

So your first step is making a plane with the resolution of the ocean you desire. If you know how to use blender, this is easy. Otherwise just add a plane object and subdivide it several times. This has the potential to really destroy performance so you’re riding a balance between how good your water looks and how well the game runs.

8k tris worked fine for me, but its straddling the edge of what is performant in my case.


Next what you want to do is give each vertex a bone. Open blender's .py text window and paste this code. It adds a bone to each vertex location and rigs the vertex to it.
import bpy
from mathutils import Vector

def AddBonesAtVertices(obj, length, use_normals):
    if not obj or obj.type != 'MESH':
        return

    points = []
    normals = []
    data = []
    for v in obj.data.vertices:
        p = obj.matrix_world @ v.co
        target = v.normal @ obj.matrix_world
        dir = target - p
        dir.normalize()
        dir = dir * length
        n = p + dir * (-1)
        points.append(p)
        if not use_normals:
            n = Vector((p[0], p[1], p[2] + length))
        normals.append(n)
        data.append([p, n])

    bpy.ops.object.mode_set(mode='OBJECT')
    
    amt = bpy.data.armatures.new(obj.name + "_vBones")
    rig = bpy.data.objects.new(obj.name + '_vRig', amt)
    
    bpy.context.collection.objects.link(rig)
    objects.active = rig
    
    names = [] #Will keep bone names
    bpy.ops.object.editmode_toggle()
    for i, l in enumerate(zip(points, normals)):
        bone = amt.edit_bones.new(str(i))
        bone.head = l[0]
        bone.tail = l[1]
        names.append(bone.name) #Add name
    bpy.ops.object.editmode_toggle()

    for v_index, name in enumerate(names):
        #Get the group
        group = obj.vertex_groups.new(name=name)
        #Link the vertex to it
        group.add([v_index], 1, 'REPLACE')

    #Parent and add modifier
    obj.parent = rig
    modifier = obj.modifiers.new(rig.name, "ARMATURE")
    modifier.object = rig
    

objects = bpy.context.view_layer.objects
obj = objects.active

AddBonesAtVertices(obj, 0.5, False)

It may take a while to run.


The result should look like this when it finishes processing.


If you plan to have your ocean textured, now is a good time to edit the UV of the plane and scale it up so more repetitions of whichever texture you use will be applied, and the smaller/clearer the textures become.


Next, you're going to Export as FBX. and put that off to the side for now. Open roblox studio and import an avatar using the avatar importer plugin. That way the bones are sorted out for you. Pick Custom.

image


Upload the fbx you set aside and it will enter your workspace

Make sure everything is there and rigged correctly. All 4k of my bones were there and the mesh deformed when I moved them.



Phase 2 is animation. There’s many ways to accomplish this, but you don’t actually have to use the animator at this point. I accomplished my effect by looping through all of the bones and applying a Gerstner Wave fomula. I won’t share my code because its very inefficient and the formula was made incorrectly, so it would be no help, but I think this is a good start to get you where you need to be.

My final outcome. I used the same formula to calculate where the floating objects should be. Real simple since it takes an X and a Z value and spits out what the Y value should be when taking into account a "C" value which is time. I put that Y value in a body position.


Some caveats to be aware of:

  • If you do use a wave formula like I suggested, you will need to run it on the client or it will be jittery and slow the server down.
  • If its run on the client, you will need to synchronize “C” (time) be the same between all clients. This can be done with a client/server clock synchonizer. A quick search will return a couple options.
  • Since C is just a variable like tick() or os.time() (hopefully synchronized at this point), you can counter lag or travel-time for body positions by consistently adding or subtracting a number from C. C+/-1 for instance.
  • Despite being a meshpart, this water unfortunately DOES NOT have collisions. It acts similarly to a special mesh in that it doesn’t react to raycasting or collisions.

Ocean Mesh

FBXImportGeneric - Roblox

I hope this helped.

232 Likes

That texture reminds me of a game called “Legend Of Zelda: The Wind Waker”

Thanks for making this tutorial, love it!

10 Likes

That’s what it was inspired by. I’m glad it helped you.

3 Likes

this is great! but can you send the Roblox place file to this please?

6 Likes

seems pretty good, most likely will be fun making the formula.

Does this run better than Roblox terrain water?

1 Like

Thanks for making this. Ive been trying to figure this out for a while now with no luck. I dont want to use terrain water or a simple part water for my builds, because it either looks too flat or too uncanny. Cant wait to try this out later on.

Do you know how performant it is?

2 Likes

yeah if you can use it right, you can make certain areas have larger waves than others though you’ll need to script it.

1 Like

The performance really depends on how you script it. If your code is really optimized and efficient, it’s better than using terrain water for a big swath of ocean.

Generally, I get spend a lot of time loading and get lag spikes when playing games that use terrain water oceans. With mesh water, though, I scripted a way for the water to be infinite in size and the loading was as though nothing was there. Also had no lag spikes.

I’m by no means a good scripter, so if I can make it work better than terrain water on my machine, I’d say its better.

1 Like

What code did you use to make it preformat?

3 Likes

I haven’t fully got it working yet, but it seems to perform well. Eventually I’m going to have bones closer towards points of interest and farther away the farther out which should help. Figuring out swimming collisions will be fun.

3 Likes

I am busy rn but you could you use perlin wave formula to make it look like a actual wave and some sine formula looking stuff

2 Likes

Actually, a Gernster is what you are going to want to use for this.

2 Likes

Actually, gerstners provide linear motion unlike actual oceans :confused:

2 Likes

But gernsters formula allows for wave crests, unlike a peeling noise formula. Also, Perlin noise is more for land and stuff. So if you are looking to create more realistic waves with actual wave CRESTS, a perlin noise algorithm is not the way to go. :slight_smile:

1 Like

Well if you want crests you could always change your noise density :slight_smile:

1 Like

That wouldn’t make accurate crests, that would just increase the overall height. The whole point of crests are being fo it tie is quickly and crest. Using perlin noise would just make the waves taller, and that’s not what a crest is. :slight_smile:

3 Likes

Thanks, super useful to know! I will definitely be using.

2 Likes

A lot of people need some assistance with the wave part, so here:
I’m not too great with scripting, so any direct scripting advice I give will actually probably be a detriment even if it makes your ocean have individual waves.

I can suggest searching “sine wave” in the roblox library and see what some of those use for their formula. You could get some ideas by tinkering with different scripts like I did.

The general idea is that you want to make each bone have a unique Y value. You do this by making the Y value dependent on the bone’s X and Z values if that makes sense.

You can look at this equation for a simple sine wave.
image

y is the Y value of any point
x is the X value of any point

'a' is the amplitude of the wave. That basically means how high and low the crests and troughs are

392f6207e8f19f0f1da99fcce1d257e0

'b' is the frequency of the wave. That basically means how close each peak and valley is to each other.

'c' is a modifier similar to "time". You change the C value and the wave moves. (its more complicated but that's essentially how you can look at it)

All you do is place any number you want for x,b,c and you get what y should be.
Since you want to make an ocean out of it, you want y to be constantly moving in a consistent and predictable way.

X will be the X value of the bone (and make room for Z, being the Z value of the bone)
A, the amplitude, will be whatever you want (good for weather conditions)
B, the frequency, will be whatever you want (good for weather conditions)
C will be time. tick() or os.time() works, but again they aren’t synchronized between clients, so different players will see the wave at different times which ruins the look of collisions.

You get the Y value and plug that into the bone’s Y value and you have a waving ocean.


Also bear in mind that this is a basic sine wave formula and not a Gerstner wave formula. I haven’t actually done a gerstner wave formula if you can see in my original finished product gif. If anyone can help me with that, that would be great.

10 Likes

Thanks. I have tried some other formula to create my waves but this one is the superior.

1 Like