@@ -6,19 +6,21 @@ weight: 920
66
77{{<video "demo.webm" >}}
88
9- For the intro to a game I (perodically) work on, I decided to put the player on
10- a pirate ship. Scrolling textures on a big plane looked pretty boring. To make
11- things a bit angrier I decided to add waves.
9+ During the intro to a game I (perodically) work on, the player starts on a
10+ pirate ship. Realistic water doesn't fit the style, and scroling textures on a
11+ big plane looked pretty boring. To make things a bit angrier I needed intense
12+ waves, and I needed those waved to actually affect the world.
1213
1314## Waves
1415
15- The visual component is the most common technique for basic waves: displacing
16- the height of each vertex on a subdivided plane .
16+ To keep it simple, we'll just do a procedural heightmap; i.e. set the vertical coorinate to
17+ be a function of the horizontal position and time .
1718
18- Inspired by [ sum of sines] ( https://developer.nvidia.com/gpugems/gpugems/part-i-natural-effects/chapter-1-effective-water-simulation-physical-models#:~:text=The%20sum%20of%20sines%20gives,to%20the%20continuous%20water%20surface. )
19- which can make semi-realistic ocean shaders, I'm taking the basic idea of adding two waves
20- with different frequencies to break up the repetitiveness. The result is good enough for this
21- cartoon style.
19+ [ Sum of
20+ sines] ( https://developer.nvidia.com/gpugems/gpugems/part-i-natural-effects/chapter-1-effective-water-simulation-physical-models#:~:text=The%20sum%20of%20sines%20gives,to%20the%20continuous%20water%20surface. )
21+ can add some semi-realistic texture to the surface of the water. I'm not doing
22+ that, but I am taking the basic idea of adding two waves with different
23+ frequencies to break up the repetitiveness.
2224
2325
2426``` glsl
@@ -38,16 +40,20 @@ void vertex() {
3840
3941## Floating
4042
41- There are two components to making objects float on top of our waves:
43+ The fun part is making the waves interactive. There are two components to
44+ making objects float on top of our waves:
4245
4346* Position: move the object up and down so it sits on top of the water.
4447* Rotation: orient the object according to the slope of the waves.
4548
4649We generally want some of the object to sit beneath the surface of the water.
47- Instead of doing anything fancy like using volume to calculate actual buoyancy,
48- we can just define our own y-offset from the model's postition, and a 2D size
49- of the "buoyancy-plane". In Godot, ` CSGBox3D ` conveiently lets us visualize and edit
50- these properties.
50+ There are more realistic buoyancy simulations that figure out the displacement according
51+ to the amount of a volume that sits beneath the surface. The complexity and noise that
52+ comes with an accurate solution doesn't fit the style, so instead we're going to fit a plane to
53+ the waves surface.
54+
55+ It's easy to adjust the plane within the Godot editor, without doing anything
56+ custom, by defining a CSGBox3D that is thrown away at runtime.
5157
5258![ CSGBox3D as a buoyancy plane] ( boundingplane.png )
5359
@@ -85,27 +91,26 @@ because I assume the shape of my rectangle is longer on that axis.
8591
8692``` gdscript
8793# find the corners (the rotation puts us into global space, needed by _wave)
88- var front_r = center + (Vector3(size.x, 0, size.y) / 2.0).rotated(Vector3.UP, plane.global_rotation.y)
89- var front_l = center + (Vector3(-size.x, 0, size.y) / 2.0).rotated(Vector3.UP, plane.global_rotation.y)
90- var back_r = center + (Vector3(size.x, 0, -size.y) / 2.0).rotated(Vector3.UP, plane.global_rotation.y)
91- var back_l = center + (Vector3(-size.x, 0, -size.y) / 2.0).rotated(Vector3.UP, plane.global_rotation.y)
92-
93- # project the points onto the wave
94- front_r.y = _wave(front_r)
95- front_l.y = _wave(front_l)
96- back_l.y = _wave(back_l)
97- back_r.y = _wave(back_r)
94+ # Define corners in local space
95+ var local_corners = [
96+ Vector3( half_x, 0, half_z), Vector3(-half_x, 0, half_z), # front
97+ Vector3(-half_x, 0, -half_z), Vector3( half_x, 0, -half_z), # back
98+ ]
99+
100+ # Rotate and add to center (global space), then apply wave
101+ for i in range(local_corners.size()):
102+ local_corners[i] = center + local_corners[i].rotated(Vector3.UP, rot_y)
103+ local_corners[i].y = _wave(local_corners[i])
104+
105+ # Also project center on the wave
98106center.y = _wave(center)
99107
100- # average normals from the triangles
101- var normal_f = (front_l - center).cross(front_r - center).normalized();
102- var normal_b = (back_r - center).cross(back_l - center).normalized();
103-
108+ # Compute the two triangle normals
109+ var normal_f = (local_corners[1] - center).cross(local_corners[0] - center).normalized()
110+ var normal_b = (local_corners[3] - center).cross(local_corners[2] - center).normalized()
104111
105- # rotation based on avg of cross products of triangles in the plane
106- var normal = ((normal_b + normal_f) / 2.0)
107- # undo the global space transformation
108- normal = normal.rotated(Vector3.UP, -plane.global_rotation.y).normalized()
112+ # Average the normals, then undo the global space rotation
113+ var normal = ((normal_f + normal_b) * 0.5).rotated(Vector3.UP, -rot_y).normalized()
109114```
110115
111116{{< gallery >}}
@@ -117,23 +122,27 @@ normal = normal.rotated(Vector3.UP, -plane.global_rotation.y).normalized()
117122From left to right are examples using the back normal, the averaged normals and
118123the front normal on top of a sharp peak.
119124
125+ To be scientific about it, I plotted a "smoothness" field, the gradient of the computed
126+ normal-field. Not very surprising, sampling a bigger area results in smoother changes.
127+ The cross-product approach had the lowest variance, and spreads the variance around a larger
128+ space.
129+
130+ ![ smoothness graph] ( smooth_field.png )
131+
120132### Position
121133
122- Assuming our buoyancy-plane is already offset from the model, and we use
123- ` position.y = wave(position.xy) ` , concave parts of the curve will have the boat
124- dip into the water on each side.
134+ A naive ` position.y = wave(position.xy) ` means that at concave parts of the curve,
135+ our plane will undernath the surface.
125136
126137{{< gallery >}}
127138 <img src =" finite_diffs_width.png " class =" grid-w50 " />
128139 <img src =" adjust_pos.png " class =" grid-w50 " />
129140{{< /gallery >}}
130141
131- We should re-frame our goal to be none of the or extremes of our bounding shape
132- dip under the water. If we instead use the mean of our samples, we fix the concave case but now we'll
133- end up submerged in convex areas.
134-
135- To get the best of both, we just take the ` max ` of the center sample or the edge
136- samples ([ desmos] ( https://www.desmos.com/calculator/y4neofo1zw ) ).
142+ If we instead use the mean of our samples, we fix the concave case but now
143+ we'll end up submerged in convex areas. To get the best of both, we just take
144+ the ` max ` of the center sample or the edge samples' mean
145+ ([ desmos] ( https://www.desmos.com/calculator/y4neofo1zw ) ).
137146
138147``` gdscript
139148# for central differences, re-use the samples
@@ -240,11 +249,15 @@ parent.velocity.y = impulse.y
240249
241250{{<video "swim.webm" >}}
242251
243- ## Conclusion
252+ ## What's next
253+
254+ * Make some levels with this!
255+ * Swimming/water levels. Diving?
256+ * Make it so you can drive the ship.
257+ * Taking advantage of the waves to gain some height could be a game mechanic.
258+ * A better surface shader, that uses depth and a bit of transparency.
259+ * ~~ Foam.~~ I made foam!
260+
261+
262+ ![ foam] ( foam.png )
244263
245- Both the game's frame-budget and my own free time to work on personal projects
246- are limited. It might be fun to implement a proper plane fitting algorithm such
247- as [ Least Squares] ( https://en.wikipedia.org/wiki/Least_squares ) or [ Principal
248- Component
249- Analysis] ( https://en.wikipedia.org/wiki/Principal_component_analysis ) , but the
250- added fidelity (and noise) aren't worth the effort for this style of game.
0 commit comments