@threenative/terrain 0.0.0-stage → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (125) hide show
  1. package/AGENT_GUIDE.md +194 -0
  2. package/LICENSE +21 -0
  3. package/README.md +71 -2
  4. package/THIRD_PARTY_NOTICES.md +38 -0
  5. package/dist/editor/index.d.ts +231 -0
  6. package/dist/editor/index.js +5624 -0
  7. package/dist/editor/server.d.ts +3 -0
  8. package/dist/editor/server.js +3688 -0
  9. package/dist/editor/worker.d.ts +2 -0
  10. package/dist/editor/worker.js +2212 -0
  11. package/dist/export.d.ts +54 -0
  12. package/dist/export.js +297 -0
  13. package/dist/index.d.ts +335 -0
  14. package/dist/index.js +2927 -0
  15. package/dist/server-ByM2hmYJ.d.ts +630 -0
  16. package/dist/terrain-gPnkCU6S.d.ts +141 -0
  17. package/dist/three.d.ts +14 -0
  18. package/dist/three.js +40 -0
  19. package/dist/types-Ant8-Axc.d.ts +486 -0
  20. package/package.json +86 -3
  21. package/starter/alpine/README.md +29 -0
  22. package/starter/alpine/assets.json +12 -0
  23. package/starter/alpine/bake.mjs +118 -0
  24. package/starter/alpine/recipe.json +66167 -0
  25. package/starter/alpine/sky.ts +23 -0
  26. package/starter/alpine/surface.json +53 -0
  27. package/starter/alpine/world.ts +335 -0
  28. package/starter/coastal/README.md +37 -0
  29. package/starter/coastal/assets.json +10 -0
  30. package/starter/coastal/bake.mjs +118 -0
  31. package/starter/coastal/recipe.json +66161 -0
  32. package/starter/coastal/sea.ts +327 -0
  33. package/starter/coastal/sky.ts +24 -0
  34. package/starter/coastal/surface.json +41 -0
  35. package/starter/coastal/world.ts +386 -0
  36. package/starter/desert/README.md +35 -0
  37. package/starter/desert/assets.json +12 -0
  38. package/starter/desert/bake.mjs +118 -0
  39. package/starter/desert/recipe.json +66156 -0
  40. package/starter/desert/sky.ts +23 -0
  41. package/starter/desert/surface.json +54 -0
  42. package/starter/desert/world.ts +335 -0
  43. package/starter/forest/README.md +46 -0
  44. package/starter/forest/assets.json +32 -0
  45. package/starter/forest/bake.mjs +118 -0
  46. package/starter/forest/recipe.json +4586 -0
  47. package/starter/forest/sky.ts +27 -0
  48. package/starter/forest/surface.json +79 -0
  49. package/starter/forest/water.ts +266 -0
  50. package/starter/forest/world.ts +413 -0
  51. package/starter/tundra/README.md +29 -0
  52. package/starter/tundra/assets.json +12 -0
  53. package/starter/tundra/bake.mjs +118 -0
  54. package/starter/tundra/recipe.json +66325 -0
  55. package/starter/tundra/sky.ts +27 -0
  56. package/starter/tundra/surface.json +54 -0
  57. package/starter/tundra/water.ts +313 -0
  58. package/starter/tundra/world.ts +366 -0
  59. package/starter-assets/bark_brown_02/bark_brown_02_diff_1k.jpg +0 -0
  60. package/starter-assets/bark_brown_02/bark_brown_02_nor_gl_1k.jpg +0 -0
  61. package/starter-assets/bark_brown_02/bark_brown_02_rough_1k.jpg +0 -0
  62. package/starter-assets/boulder_01/boulder-mid.glb +0 -0
  63. package/starter-assets/boulder_01/boulder-near.glb +0 -0
  64. package/starter-assets/cliff_side/cliff_side_ao_1k.jpg +0 -0
  65. package/starter-assets/cliff_side/cliff_side_diff_1k.jpg +0 -0
  66. package/starter-assets/cliff_side/cliff_side_nor_gl_1k.jpg +0 -0
  67. package/starter-assets/cliff_side/cliff_side_rough_1k.jpg +0 -0
  68. package/starter-assets/credits.json +1022 -0
  69. package/starter-assets/fern_02/fern_02_alpha_512.png +0 -0
  70. package/starter-assets/fern_02/fern_02_diff_512.jpg +0 -0
  71. package/starter-assets/fern_02/fern_02_nor_gl_512.jpg +0 -0
  72. package/starter-assets/fir_tree_01/fir-b-lod1.glb +0 -0
  73. package/starter-assets/fir_tree_01/fir-b-lod2.glb +0 -0
  74. package/starter-assets/fir_tree_01/fir-c-lod1.glb +0 -0
  75. package/starter-assets/fir_tree_01/fir-c-lod2.glb +0 -0
  76. package/starter-assets/fir_tree_01/fir_tree_01_twig_arm_1k.jpg +0 -0
  77. package/starter-assets/fir_tree_01/fir_tree_01_twig_diff_1k.jpg +0 -0
  78. package/starter-assets/fir_tree_01/fir_tree_01_twig_nor_gl_1k.jpg +0 -0
  79. package/starter-assets/forest_ground_04/forest_ground_04_ao_1k.jpg +0 -0
  80. package/starter-assets/forest_ground_04/forest_ground_04_diff_1k.jpg +0 -0
  81. package/starter-assets/forest_ground_04/forest_ground_04_nor_gl_1k.jpg +0 -0
  82. package/starter-assets/forest_ground_04/forest_ground_04_rough_1k.jpg +0 -0
  83. package/starter-assets/forest_leaves_02/forest_leaves_02_arm_512.jpg +0 -0
  84. package/starter-assets/forest_leaves_02/forest_leaves_02_diff_512.jpg +0 -0
  85. package/starter-assets/forest_leaves_02/forest_leaves_02_nor_gl_512.jpg +0 -0
  86. package/starter-assets/grass_medium_02/grass_medium_02_alpha_512.png +0 -0
  87. package/starter-assets/grass_medium_02/grass_medium_02_diff_512.jpg +0 -0
  88. package/starter-assets/grass_medium_02/grass_medium_02_nor_gl_512.jpg +0 -0
  89. package/starter-assets/hdri/kloofendal_48d_partly_cloudy_1k.hdr +0 -0
  90. package/starter-assets/leafy_grass/leafy_grass_ao_1k.jpg +0 -0
  91. package/starter-assets/leafy_grass/leafy_grass_diff_1k.jpg +0 -0
  92. package/starter-assets/leafy_grass/leafy_grass_nor_gl_1k.jpg +0 -0
  93. package/starter-assets/leafy_grass/leafy_grass_rough_1k.jpg +0 -0
  94. package/starter-assets/lichen_rock/lichen_rock_arm_512.jpg +0 -0
  95. package/starter-assets/lichen_rock/lichen_rock_diff_512.jpg +0 -0
  96. package/starter-assets/lichen_rock/lichen_rock_nor_gl_512.jpg +0 -0
  97. package/starter-assets/mossy_rock/mossy_rock_ao_1k.jpg +0 -0
  98. package/starter-assets/mossy_rock/mossy_rock_diff_1k.jpg +0 -0
  99. package/starter-assets/mossy_rock/mossy_rock_nor_gl_1k.jpg +0 -0
  100. package/starter-assets/mossy_rock/mossy_rock_rough_1k.jpg +0 -0
  101. package/starter-assets/needle-atlas.png +0 -0
  102. package/starter-assets/needle-surface.png +0 -0
  103. package/starter-assets/river_small_rocks/river_small_rocks_arm_512.jpg +0 -0
  104. package/starter-assets/river_small_rocks/river_small_rocks_diff_512.jpg +0 -0
  105. package/starter-assets/river_small_rocks/river_small_rocks_nor_gl_512.jpg +0 -0
  106. package/starter-assets/rock_boulder_dry/rock_boulder_dry_arm_512.jpg +0 -0
  107. package/starter-assets/rock_boulder_dry/rock_boulder_dry_diff_512.jpg +0 -0
  108. package/starter-assets/rock_boulder_dry/rock_boulder_dry_nor_gl_512.jpg +0 -0
  109. package/starter-assets/rocks/boulder-mid.glb +0 -0
  110. package/starter-assets/rocks/boulder-near.glb +0 -0
  111. package/starter-assets/rocks/boulder-prep.json +37 -0
  112. package/starter-assets/rocks/rock01-mid.glb +0 -0
  113. package/starter-assets/rocks/rock01-near.glb +0 -0
  114. package/starter-assets/rocks/rock01-prep.json +37 -0
  115. package/starter-assets/rocks/rock04-mid.glb +0 -0
  116. package/starter-assets/rocks/rock04-near.glb +0 -0
  117. package/starter-assets/rocks/rock04-prep.json +37 -0
  118. package/starter-assets/sand_01/sand_01_ao_1k.jpg +0 -0
  119. package/starter-assets/sand_01/sand_01_diff_1k.jpg +0 -0
  120. package/starter-assets/sand_01/sand_01_nor_gl_1k.jpg +0 -0
  121. package/starter-assets/sand_01/sand_01_rough_1k.jpg +0 -0
  122. package/starter-assets/snow_02/snow_02_ao_1k.jpg +0 -0
  123. package/starter-assets/snow_02/snow_02_diff_1k.jpg +0 -0
  124. package/starter-assets/snow_02/snow_02_nor_gl_1k.jpg +0 -0
  125. package/starter-assets/snow_02/snow_02_rough_1k.jpg +0 -0
@@ -0,0 +1,327 @@
1
+ // The coastal kit's sea: the installed spectral simulation drives a lit water surface. It is the example
2
+ // game's ocean, ported so the kit draws what the simulation computes: a game-owned ocean source, the
3
+ // installed SpectralOcean, and a lit water mesh. The vertex stage reads the simulation's cascade buffers, so
4
+ // what is drawn is the field the height query reads. The bake's heights decide the shoreline, the surf and
5
+ // the depth tint. Change `SEA`, `SURFACE` or `OCEAN_LOOK` to change the sea; nothing in a package decides it.
6
+ import type { ICtx, ISpectralOceanOptions } from "@threenative/core";
7
+ import { SpectralOcean } from "@threenative/core";
8
+ import type { IPhysicsContext } from "@threenative/physics";
9
+ import {
10
+ Color,
11
+ DataTexture,
12
+ DataUtils,
13
+ HalfFloatType,
14
+ LinearFilter,
15
+ Mesh,
16
+ PlaneGeometry,
17
+ RGBAFormat,
18
+ } from "three";
19
+ import {
20
+ cameraPosition,
21
+ clamp,
22
+ color,
23
+ float,
24
+ mix,
25
+ mx_fractal_noise_float,
26
+ mx_noise_float,
27
+ normalize,
28
+ positionLocal,
29
+ positionWorld,
30
+ smoothstep,
31
+ texture,
32
+ time,
33
+ transformNormalToView,
34
+ uniform,
35
+ vec2,
36
+ vec3,
37
+ } from "three/tsl";
38
+ import type { Node } from "three/webgpu";
39
+ import { MeshStandardNodeMaterial } from "three/webgpu";
40
+
41
+ /** The sea state: two non-overlapping wave bands, as the example game uses. */
42
+ export const SEA = {
43
+ amplitude: 0.0018,
44
+ cascades: [{ patchSize: 190 }, { patchSize: 37 }],
45
+ choppiness: 1.2,
46
+ directionality: 2.6,
47
+ gravity: 9.81,
48
+ readbackEveryFrames: 3,
49
+ readbackResolution: 32,
50
+ resolution: 128,
51
+ seed: 20_260_906,
52
+ smallWaveCutoff: 0.32,
53
+ windDirection: 0.55,
54
+ windSpeed: 10.5,
55
+ } satisfies ISpectralOceanOptions;
56
+
57
+ /** Dense inner grid; its outer vertices stretch to the distant horizon. */
58
+ export const SURFACE = { segments: 512, size: 1024 } as const;
59
+
60
+ /** The sea's two body colours as live uniforms, so the game can retint the water without rebuilding it. */
61
+ export const OCEAN_LOOK = {
62
+ deep: uniform(new Color(0x082e45)),
63
+ shallow: uniform(new Color(0x24646a)),
64
+ };
65
+
66
+ /** Thin breaking surf on exposed shores. */
67
+ const FOAM = 0xe9f4f6;
68
+
69
+ /**
70
+ * What the surface reads from the bake: the heights on the world's grid, row-major by Z then X, in metres,
71
+ * with the sea level and the world's size.
72
+ */
73
+ export interface ICoastalSeaData {
74
+ readonly heights: Float32Array;
75
+ readonly resolution: number;
76
+ readonly size: number;
77
+ readonly waterLevel: number;
78
+ }
79
+
80
+ /** The sea in the scene. `dispose` removes the surface, releases the simulation and the surface's textures. */
81
+ export interface ICoastalSea {
82
+ readonly mesh: Mesh;
83
+ readonly ocean: SpectralOcean;
84
+ dispose(): void;
85
+ }
86
+
87
+ /** Bilinear periodic sampling; wrap twice because JavaScript/WGSL remainder can be negative. */
88
+ function cascadeAt(
89
+ ocean: SpectralOcean,
90
+ index: number,
91
+ x: Node<"float">,
92
+ z: Node<"float">,
93
+ ): Node<"vec4"> {
94
+ const grid = float(ocean.resolution);
95
+ const patch = float(ocean.cascadePatchSize(index));
96
+ const buffer = ocean.cascadeDisplacement(index);
97
+ const u = x.div(patch).mul(grid);
98
+ const v = z.div(patch).mul(grid);
99
+ const u0 = u.floor();
100
+ const v0 = v.floor();
101
+ const wrap = (value: Node<"float">): Node<"float"> => value.mod(grid).add(grid).mod(grid);
102
+ const read = (cx: Node<"float">, cz: Node<"float">): Node<"vec4"> =>
103
+ buffer.element(wrap(cz).mul(grid).add(wrap(cx)).toUint()) as Node<"vec4">;
104
+ const near = mix(read(u0, v0), read(u0.add(1), v0), u.sub(u0));
105
+ const far = mix(read(u0, v0.add(1)), read(u0.add(1), v0.add(1)), u.sub(u0));
106
+ return mix(near, far, v.sub(v0)) as Node<"vec4">;
107
+ }
108
+
109
+ /** Summed displacement of both cascades at a world position. */
110
+ function displacementAt(ocean: SpectralOcean, x: Node<"float">, z: Node<"float">): Node<"vec3"> {
111
+ const broad = cascadeAt(ocean, 0, x, z);
112
+ const fine = cascadeAt(ocean, 1, x, z);
113
+ return vec3(broad.x.add(fine.x), broad.y.add(fine.y), broad.z.add(fine.z));
114
+ }
115
+
116
+ /**
117
+ * The sea surface: displaced by the simulation, and lit by the scene. The vertex stage reads the cascade
118
+ * buffers directly, so what is drawn is the same field the height query is copied from.
119
+ */
120
+ function createSeaMesh(ocean: SpectralOcean, data: ICoastalSeaData): Mesh {
121
+ const level = data.waterLevel;
122
+ if (!Number.isFinite(level)) throw new Error("Coastal bake has no finite sea level");
123
+ // Distance to dry land and connection to deep open water, measured once from the bake. Flooding only
124
+ // through more than 2 m of water leaves sheltered lagoons out of the breaking-surf mask.
125
+ const n = data.resolution;
126
+ const cell = data.size / (n - 1);
127
+ const distance = Float32Array.from(data.heights, (height) => (height >= level ? 0 : 1e4));
128
+ const exposed = new Uint8Array(n * n);
129
+ const queue: number[] = [];
130
+ for (let i = 0; i < n * n; i++) {
131
+ const row = Math.floor(i / n);
132
+ const col = i % n;
133
+ if (
134
+ (row === 0 || col === 0 || row === n - 1 || col === n - 1) &&
135
+ (data.heights[i] as number) < level - 2
136
+ ) {
137
+ exposed[i] = 1;
138
+ queue.push(i);
139
+ }
140
+ if (col > 0) distance[i] = Math.min(distance[i] as number, (distance[i - 1] as number) + cell);
141
+ if (row > 0) distance[i] = Math.min(distance[i] as number, (distance[i - n] as number) + cell);
142
+ }
143
+ for (let i = n * n - 1; i >= 0; i--) {
144
+ if (i % n < n - 1)
145
+ distance[i] = Math.min(distance[i] as number, (distance[i + 1] as number) + cell);
146
+ if (i < n * (n - 1))
147
+ distance[i] = Math.min(distance[i] as number, (distance[i + n] as number) + cell);
148
+ }
149
+ for (let head = 0; head < queue.length; head++) {
150
+ const i = queue[head] as number;
151
+ for (const next of [i % n > 0 ? i - 1 : -1, i % n < n - 1 ? i + 1 : -1, i - n, i + n]) {
152
+ if (
153
+ next < 0 ||
154
+ next >= exposed.length ||
155
+ exposed[next] ||
156
+ (data.heights[next] as number) >= level - 2
157
+ )
158
+ continue;
159
+ exposed[next] = 1;
160
+ queue.push(next);
161
+ }
162
+ }
163
+ // Carry exposure through the last eighteen metres of shallow water, never across a dry sand bar.
164
+ for (let pass = 0; pass < Math.ceil(18 / cell); pass++) {
165
+ const previous = exposed.slice();
166
+ for (let i = 0; i < exposed.length; i++) {
167
+ if ((data.heights[i] as number) >= level) continue;
168
+ if (
169
+ (i % n > 0 && previous[i - 1]) ||
170
+ (i % n < n - 1 && previous[i + 1]) ||
171
+ previous[i - n] ||
172
+ previous[i + n]
173
+ )
174
+ exposed[i] = 1;
175
+ }
176
+ }
177
+ const pixels = new Uint16Array(n * n * 4);
178
+ for (let i = 0; i < n * n; i++) {
179
+ pixels[i * 4] = DataUtils.toHalfFloat(data.heights[i] as number);
180
+ pixels[i * 4 + 1] = DataUtils.toHalfFloat(distance[i] as number);
181
+ pixels[i * 4 + 2] = DataUtils.toHalfFloat(exposed[i] as number);
182
+ }
183
+ const heightTexture = new DataTexture(pixels, n, n, RGBAFormat, HalfFloatType);
184
+ heightTexture.minFilter = LinearFilter;
185
+ heightTexture.magFilter = LinearFilter;
186
+ heightTexture.needsUpdate = true;
187
+ const geometry = new PlaneGeometry(
188
+ SURFACE.size,
189
+ SURFACE.size,
190
+ SURFACE.segments,
191
+ SURFACE.segments,
192
+ );
193
+ geometry.rotateX(-Math.PI / 2);
194
+ // Dense near shore, sparse beyond it; the sea reaches the fogged horizon instead of ending at the world's edge.
195
+ const vertices = geometry.getAttribute("position");
196
+ const stretch = (value: number): number =>
197
+ Math.sign(value) *
198
+ (Math.abs(value) <= 320 ? Math.abs(value) : 320 + ((Math.abs(value) - 320) / 192) ** 2 * 3800);
199
+ for (let i = 0; i < vertices.count; i++)
200
+ vertices.setXYZ(i, stretch(vertices.getX(i)), 0, stretch(vertices.getZ(i)));
201
+ geometry.computeBoundingSphere();
202
+
203
+ const material = new MeshStandardNodeMaterial({
204
+ metalness: 0,
205
+ transparent: true,
206
+ depthWrite: true,
207
+ roughness: 0.12,
208
+ });
209
+ const shoreUV = (x: Node<"float">, z: Node<"float">) =>
210
+ vec2(x.div(data.size).add(0.5), z.div(data.size).add(0.5));
211
+ const inside = (x: Node<"float">, z: Node<"float">) =>
212
+ float(1).sub(smoothstep(data.size / 2 - 2, data.size / 2 + 2, x.abs().max(z.abs())));
213
+ const vertexLand = texture(heightTexture, shoreUV(positionLocal.x, positionLocal.z)).r;
214
+ const vertexDepth = mix(
215
+ float(28),
216
+ float(level).sub(vertexLand),
217
+ inside(positionLocal.x, positionLocal.z),
218
+ );
219
+ const shoal = smoothstep(0.15, 3, vertexDepth);
220
+ const vertexDistance = positionLocal.xz.sub(cameraPosition.xz).length();
221
+ const swellFade = float(1).sub(smoothstep(240, 1600, vertexDistance));
222
+ material.positionNode = positionLocal.add(
223
+ displacementAt(ocean, positionLocal.x, positionLocal.z).mul(shoal).mul(swellFade),
224
+ );
225
+
226
+ const eyeDistance = positionWorld.sub(cameraPosition).length();
227
+ const slopeFade = float(1).sub(smoothstep(100, 950, eyeDistance));
228
+ const step = float(0.45).add(eyeDistance.mul(0.006));
229
+ const east = displacementAt(ocean, positionWorld.x.add(step), positionWorld.z);
230
+ const west = displacementAt(ocean, positionWorld.x.sub(step), positionWorld.z);
231
+ const north = displacementAt(ocean, positionWorld.x, positionWorld.z.add(step));
232
+ const south = displacementAt(ocean, positionWorld.x, positionWorld.z.sub(step));
233
+ // Small ripples have no directional period, so they do not read as corduroy.
234
+ const ripplePoint = vec3(
235
+ positionWorld.x.mul(0.85).sub(time.mul(0.22)),
236
+ positionWorld.z.mul(0.85).add(time.mul(0.11)),
237
+ time.mul(0.07),
238
+ );
239
+ const rippleHeight = (point: Node<"vec3">) =>
240
+ mx_fractal_noise_float(point, 3, 2.1, 0.48).mul(0.028);
241
+ const h = rippleHeight(ripplePoint);
242
+ const ripple = vec3(
243
+ h.sub(rippleHeight(ripplePoint.add(vec3(0.12, 0, 0)))).div(0.12),
244
+ 1,
245
+ h.sub(rippleHeight(ripplePoint.add(vec3(0, 0.12, 0)))).div(0.12),
246
+ );
247
+ const shore = texture(heightTexture, shoreUV(positionWorld.x, positionWorld.z));
248
+ const local = inside(positionWorld.x, positionWorld.z);
249
+ const depth = mix(float(28), positionWorld.y.sub(shore.r), local);
250
+ const damp = smoothstep(0.1, 2.5, depth);
251
+ const wind = mx_noise_float(
252
+ vec3(positionWorld.x.mul(0.07).sub(time.mul(0.04)), positionWorld.z.mul(0.07), time.mul(0.015)),
253
+ )
254
+ .mul(0.6)
255
+ .add(0.6);
256
+ const fineFade = float(1)
257
+ .sub(smoothstep(18, 100, eyeDistance))
258
+ .mul(damp)
259
+ .mul(wind);
260
+ const normal = normalize(
261
+ vec3(
262
+ west.y.sub(east.y).div(step.mul(2)).mul(slopeFade).mul(damp).add(ripple.x.mul(fineFade)),
263
+ 1,
264
+ south.y.sub(north.y).div(step.mul(2)).mul(slopeFade).mul(damp).add(ripple.z.mul(fineFade)),
265
+ ),
266
+ );
267
+ material.normalNode = transformNormalToView(normal);
268
+ const view = normalize(cameraPosition.sub(positionWorld));
269
+ const bounced = view.negate().reflect(normal);
270
+ // The standard material supplies Fresnel and the sun's specular lobe; the sky supplies the radiance.
271
+ material.envNode = mix(color(0xb6cbd5), color(0x568fbd), clamp(bounced.y, 0, 1)).mul(1.15);
272
+ const water = mix(OCEAN_LOOK.shallow, OCEAN_LOOK.deep, smoothstep(0.4, 9, depth));
273
+ const surf = mx_noise_float(
274
+ vec3(positionWorld.x.mul(0.9), positionWorld.z.mul(0.9), time.mul(0.45)),
275
+ )
276
+ .mul(0.5)
277
+ .add(0.5);
278
+ const shoreFoam = float(1)
279
+ .sub(smoothstep(0.25, 1.8, depth))
280
+ .mul(smoothstep(0.015, 0.08, depth))
281
+ .mul(float(1).sub(smoothstep(1, 5.5, shore.g)))
282
+ .mul(smoothstep(0.25, 0.65, surf).mul(0.7).add(0.3))
283
+ .mul(shore.b)
284
+ .mul(local)
285
+ .mul(0.8);
286
+ material.colorNode = mix(water, color(FOAM), shoreFoam);
287
+ // The surface runs to the fogged horizon, and fades into the sky there rather than ending on a hard band.
288
+ const horizonFade = float(1).sub(smoothstep(1200, 3500, eyeDistance));
289
+ material.opacityNode = smoothstep(-0.03, 0.35, depth).max(shoreFoam.mul(0.9)).mul(horizonFade);
290
+ material.roughnessNode = mix(
291
+ float(0.12).add(smoothstep(70, 700, eyeDistance).mul(0.07)),
292
+ float(0.7),
293
+ shoreFoam,
294
+ );
295
+ material.addEventListener("dispose", () => heightTexture.dispose());
296
+
297
+ const mesh = new Mesh(geometry, material);
298
+ mesh.position.y = level;
299
+ mesh.receiveShadow = true;
300
+ mesh.frustumCulled = false;
301
+ mesh.name = "sea-surface";
302
+ return mesh;
303
+ }
304
+
305
+ /**
306
+ * Adds the spectral simulation and its lit surface at the bake's sea level. The caller owns the handle.
307
+ */
308
+ export function addCoastalSea(
309
+ ctx: ICtx<Record<string, unknown>, IPhysicsContext>,
310
+ data: ICoastalSeaData,
311
+ ): ICoastalSea {
312
+ const ocean = new SpectralOcean(SEA);
313
+ ctx.add(ocean);
314
+ const mesh = createSeaMesh(ocean, data);
315
+ ctx.add(mesh);
316
+ return {
317
+ mesh,
318
+ ocean,
319
+ dispose() {
320
+ mesh.removeFromParent();
321
+ mesh.geometry.dispose();
322
+ (mesh.material as MeshStandardNodeMaterial).dispose();
323
+ ocean.detach();
324
+ ocean.removeFromParent();
325
+ },
326
+ };
327
+ }
@@ -0,0 +1,24 @@
1
+ // The coastal world's daylight: the engine's sun, sky, fill, haze and tone curve, every value this world's.
2
+ // Edit any number here to change the look. Nothing in a package decides it.
3
+ import { Daylight } from "@threenative/core";
4
+ import { ACESFilmicToneMapping, Color, type Object3D, Vector3 } from "three";
5
+
6
+ export function coastalDaylight(follow: Object3D): Daylight {
7
+ const daylight = new Daylight({
8
+ follow,
9
+ // Mid-morning sun from the south-east, high enough for short readable shadows under the firs.
10
+ sunDirection: new Vector3(0.55, 0.62, 0.56),
11
+ sunColor: new Color(0xfff1dc),
12
+ sunIntensity: 3.2,
13
+ shadowExtents: [24, 96, 320],
14
+ sky: { turbidity: 2, rayleigh: 3, mieCoefficient: 0.003, mieDirectionalG: 0.82 },
15
+ fill: { sky: new Color(0xa8c8e8), ground: new Color(0x464937), intensity: 0.62 },
16
+ // Enough air that the 512 m world's far edge fades instead of ending on a hard line.
17
+ haze: { color: new Color(0x9fb4c2), density: 0.0016 },
18
+ exposure: 2 ** -0.38,
19
+ toneMapping: ACESFilmicToneMapping,
20
+ // The box's corners must stay inside the camera's far plane (5000 m in the kit's example scene).
21
+ skySize: 5000,
22
+ });
23
+ return daylight;
24
+ }
@@ -0,0 +1,41 @@
1
+ {
2
+ "base": {
3
+ "id": "sand",
4
+ "tile": 3.4,
5
+ "tint": [1, 1, 1],
6
+ "normal": true,
7
+ "triplanar": false
8
+ },
9
+ "breakup": {
10
+ "scale": 0.05,
11
+ "push": 0.12
12
+ },
13
+ "macro": {
14
+ "scale": 0.006,
15
+ "min": 0.75,
16
+ "max": 1.1
17
+ },
18
+ "layers": [
19
+ {
20
+ "id": "rock",
21
+ "tile": 3.8,
22
+ "tint": [1, 1, 1],
23
+ "normal": true,
24
+ "triplanar": true,
25
+ "mask": "rock",
26
+ "lo": 0.25,
27
+ "hi": 0.75
28
+ }
29
+ ],
30
+ "textures": "textures",
31
+ "sources": {
32
+ "sand": {
33
+ "diff": "sand_01/sand_01_diff_1k.jpg",
34
+ "nrm": "sand_01/sand_01_nor_gl_1k.jpg"
35
+ },
36
+ "rock": {
37
+ "diff": "rock_boulder_dry/rock_boulder_dry_diff_512.jpg",
38
+ "nrm": "rock_boulder_dry/rock_boulder_dry_nor_gl_512.jpg"
39
+ }
40
+ }
41
+ }