@@ -6,19 +6,21 @@ weight: 920
6
6
7
7
{{<video "demo.webm" >}}
8
8
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.
12
13
13
14
## Waves
14
15
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 .
17
18
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.
22
24
23
25
24
26
``` glsl
@@ -38,16 +40,20 @@ void vertex() {
38
40
39
41
## Floating
40
42
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:
42
45
43
46
* Position: move the object up and down so it sits on top of the water.
44
47
* Rotation: orient the object according to the slope of the waves.
45
48
46
49
We 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.
51
57
52
58
![ CSGBox3D as a buoyancy plane] ( boundingplane.png )
53
59
@@ -85,27 +91,26 @@ because I assume the shape of my rectangle is longer on that axis.
85
91
86
92
``` gdscript
87
93
# 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
98
106
center.y = _wave(center)
99
107
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()
104
111
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()
109
114
```
110
115
111
116
{{< gallery >}}
@@ -117,23 +122,27 @@ normal = normal.rotated(Vector3.UP, -plane.global_rotation.y).normalized()
117
122
From left to right are examples using the back normal, the averaged normals and
118
123
the front normal on top of a sharp peak.
119
124
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
+
120
132
### Position
121
133
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.
125
136
126
137
{{< gallery >}}
127
138
<img src =" finite_diffs_width.png " class =" grid-w50 " />
128
139
<img src =" adjust_pos.png " class =" grid-w50 " />
129
140
{{< /gallery >}}
130
141
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 ) ).
137
146
138
147
``` gdscript
139
148
# for central differences, re-use the samples
@@ -240,11 +249,15 @@ parent.velocity.y = impulse.y
240
249
241
250
{{<video "swim.webm" >}}
242
251
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 )
244
263
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