@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.
- package/AGENT_GUIDE.md +194 -0
- package/LICENSE +21 -0
- package/README.md +71 -2
- package/THIRD_PARTY_NOTICES.md +38 -0
- package/dist/editor/index.d.ts +231 -0
- package/dist/editor/index.js +5624 -0
- package/dist/editor/server.d.ts +3 -0
- package/dist/editor/server.js +3688 -0
- package/dist/editor/worker.d.ts +2 -0
- package/dist/editor/worker.js +2212 -0
- package/dist/export.d.ts +54 -0
- package/dist/export.js +297 -0
- package/dist/index.d.ts +335 -0
- package/dist/index.js +2927 -0
- package/dist/server-ByM2hmYJ.d.ts +630 -0
- package/dist/terrain-gPnkCU6S.d.ts +141 -0
- package/dist/three.d.ts +14 -0
- package/dist/three.js +40 -0
- package/dist/types-Ant8-Axc.d.ts +486 -0
- package/package.json +86 -3
- package/starter/alpine/README.md +29 -0
- package/starter/alpine/assets.json +12 -0
- package/starter/alpine/bake.mjs +118 -0
- package/starter/alpine/recipe.json +66167 -0
- package/starter/alpine/sky.ts +23 -0
- package/starter/alpine/surface.json +53 -0
- package/starter/alpine/world.ts +335 -0
- package/starter/coastal/README.md +37 -0
- package/starter/coastal/assets.json +10 -0
- package/starter/coastal/bake.mjs +118 -0
- package/starter/coastal/recipe.json +66161 -0
- package/starter/coastal/sea.ts +327 -0
- package/starter/coastal/sky.ts +24 -0
- package/starter/coastal/surface.json +41 -0
- package/starter/coastal/world.ts +386 -0
- package/starter/desert/README.md +35 -0
- package/starter/desert/assets.json +12 -0
- package/starter/desert/bake.mjs +118 -0
- package/starter/desert/recipe.json +66156 -0
- package/starter/desert/sky.ts +23 -0
- package/starter/desert/surface.json +54 -0
- package/starter/desert/world.ts +335 -0
- package/starter/forest/README.md +46 -0
- package/starter/forest/assets.json +32 -0
- package/starter/forest/bake.mjs +118 -0
- package/starter/forest/recipe.json +4586 -0
- package/starter/forest/sky.ts +27 -0
- package/starter/forest/surface.json +79 -0
- package/starter/forest/water.ts +266 -0
- package/starter/forest/world.ts +413 -0
- package/starter/tundra/README.md +29 -0
- package/starter/tundra/assets.json +12 -0
- package/starter/tundra/bake.mjs +118 -0
- package/starter/tundra/recipe.json +66325 -0
- package/starter/tundra/sky.ts +27 -0
- package/starter/tundra/surface.json +54 -0
- package/starter/tundra/water.ts +313 -0
- package/starter/tundra/world.ts +366 -0
- package/starter-assets/bark_brown_02/bark_brown_02_diff_1k.jpg +0 -0
- package/starter-assets/bark_brown_02/bark_brown_02_nor_gl_1k.jpg +0 -0
- package/starter-assets/bark_brown_02/bark_brown_02_rough_1k.jpg +0 -0
- package/starter-assets/boulder_01/boulder-mid.glb +0 -0
- package/starter-assets/boulder_01/boulder-near.glb +0 -0
- package/starter-assets/cliff_side/cliff_side_ao_1k.jpg +0 -0
- package/starter-assets/cliff_side/cliff_side_diff_1k.jpg +0 -0
- package/starter-assets/cliff_side/cliff_side_nor_gl_1k.jpg +0 -0
- package/starter-assets/cliff_side/cliff_side_rough_1k.jpg +0 -0
- package/starter-assets/credits.json +1022 -0
- package/starter-assets/fern_02/fern_02_alpha_512.png +0 -0
- package/starter-assets/fern_02/fern_02_diff_512.jpg +0 -0
- package/starter-assets/fern_02/fern_02_nor_gl_512.jpg +0 -0
- package/starter-assets/fir_tree_01/fir-b-lod1.glb +0 -0
- package/starter-assets/fir_tree_01/fir-b-lod2.glb +0 -0
- package/starter-assets/fir_tree_01/fir-c-lod1.glb +0 -0
- package/starter-assets/fir_tree_01/fir-c-lod2.glb +0 -0
- package/starter-assets/fir_tree_01/fir_tree_01_twig_arm_1k.jpg +0 -0
- package/starter-assets/fir_tree_01/fir_tree_01_twig_diff_1k.jpg +0 -0
- package/starter-assets/fir_tree_01/fir_tree_01_twig_nor_gl_1k.jpg +0 -0
- package/starter-assets/forest_ground_04/forest_ground_04_ao_1k.jpg +0 -0
- package/starter-assets/forest_ground_04/forest_ground_04_diff_1k.jpg +0 -0
- package/starter-assets/forest_ground_04/forest_ground_04_nor_gl_1k.jpg +0 -0
- package/starter-assets/forest_ground_04/forest_ground_04_rough_1k.jpg +0 -0
- package/starter-assets/forest_leaves_02/forest_leaves_02_arm_512.jpg +0 -0
- package/starter-assets/forest_leaves_02/forest_leaves_02_diff_512.jpg +0 -0
- package/starter-assets/forest_leaves_02/forest_leaves_02_nor_gl_512.jpg +0 -0
- package/starter-assets/grass_medium_02/grass_medium_02_alpha_512.png +0 -0
- package/starter-assets/grass_medium_02/grass_medium_02_diff_512.jpg +0 -0
- package/starter-assets/grass_medium_02/grass_medium_02_nor_gl_512.jpg +0 -0
- package/starter-assets/hdri/kloofendal_48d_partly_cloudy_1k.hdr +0 -0
- package/starter-assets/leafy_grass/leafy_grass_ao_1k.jpg +0 -0
- package/starter-assets/leafy_grass/leafy_grass_diff_1k.jpg +0 -0
- package/starter-assets/leafy_grass/leafy_grass_nor_gl_1k.jpg +0 -0
- package/starter-assets/leafy_grass/leafy_grass_rough_1k.jpg +0 -0
- package/starter-assets/lichen_rock/lichen_rock_arm_512.jpg +0 -0
- package/starter-assets/lichen_rock/lichen_rock_diff_512.jpg +0 -0
- package/starter-assets/lichen_rock/lichen_rock_nor_gl_512.jpg +0 -0
- package/starter-assets/mossy_rock/mossy_rock_ao_1k.jpg +0 -0
- package/starter-assets/mossy_rock/mossy_rock_diff_1k.jpg +0 -0
- package/starter-assets/mossy_rock/mossy_rock_nor_gl_1k.jpg +0 -0
- package/starter-assets/mossy_rock/mossy_rock_rough_1k.jpg +0 -0
- package/starter-assets/needle-atlas.png +0 -0
- package/starter-assets/needle-surface.png +0 -0
- package/starter-assets/river_small_rocks/river_small_rocks_arm_512.jpg +0 -0
- package/starter-assets/river_small_rocks/river_small_rocks_diff_512.jpg +0 -0
- package/starter-assets/river_small_rocks/river_small_rocks_nor_gl_512.jpg +0 -0
- package/starter-assets/rock_boulder_dry/rock_boulder_dry_arm_512.jpg +0 -0
- package/starter-assets/rock_boulder_dry/rock_boulder_dry_diff_512.jpg +0 -0
- package/starter-assets/rock_boulder_dry/rock_boulder_dry_nor_gl_512.jpg +0 -0
- package/starter-assets/rocks/boulder-mid.glb +0 -0
- package/starter-assets/rocks/boulder-near.glb +0 -0
- package/starter-assets/rocks/boulder-prep.json +37 -0
- package/starter-assets/rocks/rock01-mid.glb +0 -0
- package/starter-assets/rocks/rock01-near.glb +0 -0
- package/starter-assets/rocks/rock01-prep.json +37 -0
- package/starter-assets/rocks/rock04-mid.glb +0 -0
- package/starter-assets/rocks/rock04-near.glb +0 -0
- package/starter-assets/rocks/rock04-prep.json +37 -0
- package/starter-assets/sand_01/sand_01_ao_1k.jpg +0 -0
- package/starter-assets/sand_01/sand_01_diff_1k.jpg +0 -0
- package/starter-assets/sand_01/sand_01_nor_gl_1k.jpg +0 -0
- package/starter-assets/sand_01/sand_01_rough_1k.jpg +0 -0
- package/starter-assets/snow_02/snow_02_ao_1k.jpg +0 -0
- package/starter-assets/snow_02/snow_02_diff_1k.jpg +0 -0
- package/starter-assets/snow_02/snow_02_nor_gl_1k.jpg +0 -0
- package/starter-assets/snow_02/snow_02_rough_1k.jpg +0 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// The forest'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
|
+
import { SKY_REFLECTION_LAYER } from "./water.js";
|
|
6
|
+
|
|
7
|
+
export function forestDaylight(follow: Object3D): Daylight {
|
|
8
|
+
const daylight = new Daylight({
|
|
9
|
+
follow,
|
|
10
|
+
// Mid-morning sun from the south-east, high enough for short readable shadows under the firs.
|
|
11
|
+
sunDirection: new Vector3(0.55, 0.62, 0.56),
|
|
12
|
+
sunColor: new Color(0xfff1dc),
|
|
13
|
+
sunIntensity: 2.6,
|
|
14
|
+
shadowExtents: [24, 96, 320],
|
|
15
|
+
sky: { turbidity: 1.2, rayleigh: 2.4, mieCoefficient: 0.0015, mieDirectionalG: 0.78 },
|
|
16
|
+
fill: { sky: new Color(0xa8c8e8), ground: new Color(0x7a7e5c), intensity: 1.5 },
|
|
17
|
+
// Enough air that the 512 m world's far edge fades instead of ending on a hard line.
|
|
18
|
+
haze: { color: new Color(0x9fb4c2), density: 0.0012 },
|
|
19
|
+
exposure: 2 ** -0.38,
|
|
20
|
+
toneMapping: ACESFilmicToneMapping,
|
|
21
|
+
// The box's corners must stay inside the camera's far plane (5000 m in the kit's example scene).
|
|
22
|
+
skySize: 5000,
|
|
23
|
+
});
|
|
24
|
+
// The lake mirrors the sky: its mirror draws only the layer the sky sits on (see water.ts).
|
|
25
|
+
daylight.sky.layers.enable(SKY_REFLECTION_LAYER);
|
|
26
|
+
return daylight;
|
|
27
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
{
|
|
2
|
+
"base": {
|
|
3
|
+
"id": "grass",
|
|
4
|
+
"tile": 4.2,
|
|
5
|
+
"tint": [0.72, 0.95, 0.55],
|
|
6
|
+
"normal": true,
|
|
7
|
+
"saturation": 1.25,
|
|
8
|
+
"far": {
|
|
9
|
+
"tile": 17,
|
|
10
|
+
"from": 4,
|
|
11
|
+
"to": 40,
|
|
12
|
+
"amount": 0.6
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"breakup": {
|
|
16
|
+
"scale": 0.05,
|
|
17
|
+
"push": 0.28
|
|
18
|
+
},
|
|
19
|
+
"macro": {
|
|
20
|
+
"scale": 0.006,
|
|
21
|
+
"min": 0.6,
|
|
22
|
+
"max": 1.28
|
|
23
|
+
},
|
|
24
|
+
"layers": [
|
|
25
|
+
{
|
|
26
|
+
"id": "dirt",
|
|
27
|
+
"tile": 3.4,
|
|
28
|
+
"tint": [0.8, 0.72, 0.62],
|
|
29
|
+
"normal": true,
|
|
30
|
+
"mask": "dirt",
|
|
31
|
+
"lo": 0.45,
|
|
32
|
+
"hi": 0.85,
|
|
33
|
+
"far": {
|
|
34
|
+
"tile": 15,
|
|
35
|
+
"from": 4,
|
|
36
|
+
"to": 40,
|
|
37
|
+
"amount": 0.5
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"id": "rock",
|
|
42
|
+
"tile": 3.8,
|
|
43
|
+
"tint": [1, 1, 1],
|
|
44
|
+
"normal": true,
|
|
45
|
+
"triplanar": true,
|
|
46
|
+
"mask": "rock",
|
|
47
|
+
"lo": 0.25,
|
|
48
|
+
"hi": 0.75
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
"id": "moss",
|
|
52
|
+
"tile": 3.6,
|
|
53
|
+
"tint": [1, 1, 1],
|
|
54
|
+
"normal": true,
|
|
55
|
+
"mask": "moss",
|
|
56
|
+
"lo": 0.25,
|
|
57
|
+
"hi": 0.75
|
|
58
|
+
}
|
|
59
|
+
],
|
|
60
|
+
"textures": "textures",
|
|
61
|
+
"sources": {
|
|
62
|
+
"grass": {
|
|
63
|
+
"diff": "leafy_grass/leafy_grass_diff_1k.jpg",
|
|
64
|
+
"nrm": "leafy_grass/leafy_grass_nor_gl_1k.jpg"
|
|
65
|
+
},
|
|
66
|
+
"dirt": {
|
|
67
|
+
"diff": "forest_ground_04/forest_ground_04_diff_1k.jpg",
|
|
68
|
+
"nrm": "forest_ground_04/forest_ground_04_nor_gl_1k.jpg"
|
|
69
|
+
},
|
|
70
|
+
"rock": {
|
|
71
|
+
"diff": "rock_boulder_dry/rock_boulder_dry_diff_512.jpg",
|
|
72
|
+
"nrm": "rock_boulder_dry/rock_boulder_dry_nor_gl_512.jpg"
|
|
73
|
+
},
|
|
74
|
+
"moss": {
|
|
75
|
+
"diff": "mossy_rock/mossy_rock_diff_1k.jpg",
|
|
76
|
+
"nrm": "mossy_rock/mossy_rock_nor_gl_1k.jpg"
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
// The forest's lake and river, drawn from the water the bake wrote to `world/water.json`. The lake covers
|
|
2
|
+
// the ground that its level floods from the lake's centre, read from the same heightfield the physics
|
|
3
|
+
// stands on, so the water sits where the bake floods it. WaterSurface3D mirrors the sky in the lake and
|
|
4
|
+
// shows the bed through it, and the water thins to nothing where the ground rises through the level: that
|
|
5
|
+
// is the shore. A river follows the ground, so it is a ribbon the game shades itself. Colours, depths and
|
|
6
|
+
// the shore are this file's to change: the kit owns its look.
|
|
7
|
+
import type { ICtx } from "@threenative/core";
|
|
8
|
+
import { WaterSurface3D } from "@threenative/core";
|
|
9
|
+
import type { Heightfield } from "@threenative/core/world";
|
|
10
|
+
import type { IPhysicsContext } from "@threenative/physics";
|
|
11
|
+
import {
|
|
12
|
+
BufferAttribute,
|
|
13
|
+
BufferGeometry,
|
|
14
|
+
Color,
|
|
15
|
+
DoubleSide,
|
|
16
|
+
type Material,
|
|
17
|
+
Mesh,
|
|
18
|
+
MeshStandardMaterial,
|
|
19
|
+
} from "three";
|
|
20
|
+
import {
|
|
21
|
+
cameraPosition,
|
|
22
|
+
clamp,
|
|
23
|
+
dot,
|
|
24
|
+
float,
|
|
25
|
+
mix,
|
|
26
|
+
normalize,
|
|
27
|
+
positionWorld,
|
|
28
|
+
pow,
|
|
29
|
+
smoothstep,
|
|
30
|
+
vec3,
|
|
31
|
+
} from "three/tsl";
|
|
32
|
+
import { MeshBasicNodeMaterial } from "three/webgpu";
|
|
33
|
+
|
|
34
|
+
/** The generator's own water, written beside the world by `bake.mjs`. */
|
|
35
|
+
export interface IForestWaterFile {
|
|
36
|
+
readonly lakes: readonly ILake[];
|
|
37
|
+
readonly rivers: readonly IRiver[];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface ILake {
|
|
41
|
+
readonly id: string;
|
|
42
|
+
readonly at: readonly [number, number];
|
|
43
|
+
readonly radius: number;
|
|
44
|
+
readonly level: number;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
interface IRiver {
|
|
48
|
+
readonly id: string;
|
|
49
|
+
readonly width: number;
|
|
50
|
+
readonly points: readonly (readonly [number, number, number])[];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface IForestWater {
|
|
54
|
+
readonly lakes: number;
|
|
55
|
+
readonly rivers: number;
|
|
56
|
+
dispose(): void;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** The layer the sky sits on, so the lake's mirror can show it (see `sky.ts`). */
|
|
60
|
+
export const SKY_REFLECTION_LAYER = 3;
|
|
61
|
+
/** The ribbon sits this far above its profile, clear of the carved bed. */
|
|
62
|
+
const RIVER_LIFT = 0.05;
|
|
63
|
+
/** Metres of water over the bed at which the lake is fully opaque; thinner water fades into the shore. */
|
|
64
|
+
const SHORE_DEPTH = 0.3;
|
|
65
|
+
/**
|
|
66
|
+
* The colour the mirrored sky takes at grazing angles, in linear light. The sky's horizon is near-neutral,
|
|
67
|
+
* so the tint carries the lake's blue; the mirror keeps the sky's gradient.
|
|
68
|
+
*/
|
|
69
|
+
const SHEEN = new Color(0x8fa2b1);
|
|
70
|
+
/** Deep water's own colour; the bed shows through where the lake is shallow. */
|
|
71
|
+
const BODY = new Color(0x1c3d3f);
|
|
72
|
+
const RIVER = new Color(0x2a4b4c);
|
|
73
|
+
/** The pale, silty band where the water thins over the bank; it reads as a wet shore, not a cut edge. */
|
|
74
|
+
const SHORE = new Color(0xb8c2b2);
|
|
75
|
+
/** Water thinner than this many metres takes the SHORE colour, fading in as it thins. */
|
|
76
|
+
const SHORE_BAND = 2;
|
|
77
|
+
|
|
78
|
+
/** The heightfield's sample grid in world metres: one node per column and row, the first at (x0, z0). */
|
|
79
|
+
function gridOf(field: Heightfield): { stepX: number; stepZ: number; x0: number; z0: number } {
|
|
80
|
+
return {
|
|
81
|
+
stepX: field.width / (field.columns - 1),
|
|
82
|
+
stepZ: field.depth / (field.rows - 1),
|
|
83
|
+
x0: field.origin.x - field.width / 2,
|
|
84
|
+
z0: field.origin.z - field.depth / 2,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* The grid nodes the lake covers: the nodes below its level that a flood from its centre reaches inside
|
|
90
|
+
* its radius. This reproduces the generator's own flood on the heightfield the physics stands on.
|
|
91
|
+
*/
|
|
92
|
+
function floodedNodes(field: Heightfield, lake: ILake): Uint8Array {
|
|
93
|
+
const { columns, rows } = field;
|
|
94
|
+
const { stepX, stepZ, x0, z0 } = gridOf(field);
|
|
95
|
+
const [cx, cz] = lake.at;
|
|
96
|
+
const worldX = (node: number): number => x0 + (node % columns) * stepX;
|
|
97
|
+
const worldZ = (node: number): number => z0 + Math.floor(node / columns) * stepZ;
|
|
98
|
+
const below = (node: number): boolean => field.heightAt(worldX(node), worldZ(node)) < lake.level;
|
|
99
|
+
const within = (node: number): boolean =>
|
|
100
|
+
Math.hypot(worldX(node) - cx, worldZ(node) - cz) <= lake.radius;
|
|
101
|
+
const start = Math.round((cz - z0) / stepZ) * columns + Math.round((cx - x0) / stepX);
|
|
102
|
+
const flooded = new Uint8Array(columns * rows);
|
|
103
|
+
const seen = new Uint8Array(columns * rows);
|
|
104
|
+
const queue: number[] = [start];
|
|
105
|
+
seen[start] = 1;
|
|
106
|
+
for (let head = 0; head < queue.length; head += 1) {
|
|
107
|
+
const node = queue[head];
|
|
108
|
+
if (node === undefined || !below(node)) continue;
|
|
109
|
+
flooded[node] = 1;
|
|
110
|
+
const i = node % columns;
|
|
111
|
+
const j = Math.floor(node / columns);
|
|
112
|
+
const neighbours = [
|
|
113
|
+
i > 0 ? node - 1 : -1,
|
|
114
|
+
i < columns - 1 ? node + 1 : -1,
|
|
115
|
+
j > 0 ? node - columns : -1,
|
|
116
|
+
j < rows - 1 ? node + columns : -1,
|
|
117
|
+
];
|
|
118
|
+
for (const next of neighbours) {
|
|
119
|
+
if (next < 0 || seen[next] === 1 || !within(next)) continue;
|
|
120
|
+
seen[next] = 1;
|
|
121
|
+
queue.push(next);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return flooded;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* The lake's surface: a level sheet over every grid cell that touches a flooded node. The sheet runs a
|
|
129
|
+
* cell past the flood, so the shore is where `lakeMaterial` thins the water, not the sheet's edge.
|
|
130
|
+
*/
|
|
131
|
+
function lakeGeometry(field: Heightfield, lake: ILake, flooded: Uint8Array): BufferGeometry {
|
|
132
|
+
const { columns, rows } = field;
|
|
133
|
+
const { stepX, stepZ, x0, z0 } = gridOf(field);
|
|
134
|
+
const positions: number[] = [];
|
|
135
|
+
const indices: number[] = [];
|
|
136
|
+
const vertexOf = new Map<number, number>();
|
|
137
|
+
const vertex = (node: number): number => {
|
|
138
|
+
const known = vertexOf.get(node);
|
|
139
|
+
if (known !== undefined) return known;
|
|
140
|
+
const index = positions.length / 3;
|
|
141
|
+
positions.push(
|
|
142
|
+
x0 + (node % columns) * stepX,
|
|
143
|
+
lake.level,
|
|
144
|
+
z0 + Math.floor(node / columns) * stepZ,
|
|
145
|
+
);
|
|
146
|
+
vertexOf.set(node, index);
|
|
147
|
+
return index;
|
|
148
|
+
};
|
|
149
|
+
for (let j = 0; j < rows - 1; j += 1)
|
|
150
|
+
for (let i = 0; i < columns - 1; i += 1) {
|
|
151
|
+
const a = j * columns + i;
|
|
152
|
+
const b = a + 1;
|
|
153
|
+
const c = a + columns;
|
|
154
|
+
const d = c + 1;
|
|
155
|
+
if (!(flooded[a] || flooded[b] || flooded[c] || flooded[d])) continue;
|
|
156
|
+
const va = vertex(a);
|
|
157
|
+
const vb = vertex(b);
|
|
158
|
+
const vc = vertex(c);
|
|
159
|
+
const vd = vertex(d);
|
|
160
|
+
indices.push(va, vc, vb, vb, vc, vd);
|
|
161
|
+
}
|
|
162
|
+
const geometry = new BufferGeometry();
|
|
163
|
+
geometry.setAttribute("position", new BufferAttribute(new Float32Array(positions), 3));
|
|
164
|
+
geometry.setIndex(indices);
|
|
165
|
+
return geometry;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* The lake's surface: the sky mirrored in it at grazing angles, the bed seen through it where it is
|
|
170
|
+
* shallow, and the body's own colour where it is deep. Schlick at water's 1.333 index, so the water at
|
|
171
|
+
* your feet is clear and the far bank is a mirror. Where the water is thinner than SHORE_DEPTH it fades
|
|
172
|
+
* to nothing, because the ground has risen through the level there.
|
|
173
|
+
*/
|
|
174
|
+
function lakeMaterial(surface: WaterSurface3D): MeshBasicNodeMaterial {
|
|
175
|
+
const view = normalize(cameraPosition.sub(positionWorld));
|
|
176
|
+
const facing = clamp(dot(vec3(0, 1, 0), view), float(0), float(1));
|
|
177
|
+
const fresnel = float(0.02).add(pow(float(1).sub(facing), 5).mul(0.98));
|
|
178
|
+
const thickness = surface.thicknessAt();
|
|
179
|
+
const body = vec3(BODY.r, BODY.g, BODY.b);
|
|
180
|
+
const submerged = mix(surface.refractionAt(), body, smoothstep(float(0.4), float(3), thickness));
|
|
181
|
+
const material = new MeshBasicNodeMaterial({ transparent: true, side: DoubleSide });
|
|
182
|
+
// The HDR mirror is compressed, then tinted: its horizon is neutral, the water should not be.
|
|
183
|
+
const mirror = surface.reflectionAt();
|
|
184
|
+
const compressed = mirror.div(mirror.add(vec3(1, 1, 1)));
|
|
185
|
+
const open = mix(submerged, compressed.mul(vec3(SHEEN.r, SHEEN.g, SHEEN.b)), fresnel);
|
|
186
|
+
const edge = float(1).sub(smoothstep(float(0.05), float(SHORE_BAND), thickness));
|
|
187
|
+
material.colorNode = mix(open, vec3(SHORE.r, SHORE.g, SHORE.b), edge.mul(0.16));
|
|
188
|
+
material.opacityNode = smoothstep(float(0), float(SHORE_DEPTH), thickness);
|
|
189
|
+
return material;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** A ribbon along the river's profile: each point's left and right edge across the course. */
|
|
193
|
+
function riverGeometry(river: IRiver): BufferGeometry {
|
|
194
|
+
const half = river.width / 2;
|
|
195
|
+
const positions: number[] = [];
|
|
196
|
+
const indices: number[] = [];
|
|
197
|
+
river.points.forEach(([x, y, z], i) => {
|
|
198
|
+
const before = river.points[Math.max(0, i - 1)] ?? [x, y, z];
|
|
199
|
+
const after = river.points[Math.min(river.points.length - 1, i + 1)] ?? [x, y, z];
|
|
200
|
+
const tx = after[0] - before[0];
|
|
201
|
+
const tz = after[2] - before[2];
|
|
202
|
+
const length = Math.hypot(tx, tz) || 1;
|
|
203
|
+
const sx = (-tz / length) * half;
|
|
204
|
+
const sz = (tx / length) * half;
|
|
205
|
+
positions.push(x + sx, y + RIVER_LIFT, z + sz, x - sx, y + RIVER_LIFT, z - sz);
|
|
206
|
+
if (i > 0) {
|
|
207
|
+
const a = 2 * (i - 1);
|
|
208
|
+
const b = 2 * i;
|
|
209
|
+
indices.push(a, b, a + 1, b, b + 1, a + 1);
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
const geometry = new BufferGeometry();
|
|
213
|
+
geometry.setAttribute("position", new BufferAttribute(new Float32Array(positions), 3));
|
|
214
|
+
geometry.setIndex(indices);
|
|
215
|
+
return geometry;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Adds the lake and river the bake wrote. `field` is the ground the props and the colliders stand on.
|
|
220
|
+
* The caller owns the returned handle: its `dispose` removes both meshes and releases each surface.
|
|
221
|
+
*/
|
|
222
|
+
export function addForestWater(
|
|
223
|
+
ctx: ICtx<Record<string, unknown>, IPhysicsContext>,
|
|
224
|
+
water: IForestWaterFile,
|
|
225
|
+
field: Heightfield,
|
|
226
|
+
): IForestWater {
|
|
227
|
+
const surfaces: WaterSurface3D[] = [];
|
|
228
|
+
const meshes: Mesh[] = [];
|
|
229
|
+
for (const lake of water.lakes) {
|
|
230
|
+
const surface = new WaterSurface3D({
|
|
231
|
+
level: lake.level,
|
|
232
|
+
maxThickness: 4,
|
|
233
|
+
// The mirror holds only the sky, which barely changes: redrawing it every fourth frame keeps the
|
|
234
|
+
// water's look and takes the second world draw off most frames (the lake's cost in the ground view).
|
|
235
|
+
reflection: { resolutionScale: 0.5, layers: 1 << SKY_REFLECTION_LAYER, refreshInterval: 4 },
|
|
236
|
+
});
|
|
237
|
+
surfaces.push(surface);
|
|
238
|
+
meshes.push(
|
|
239
|
+
new Mesh(lakeGeometry(field, lake, floodedNodes(field, lake)), lakeMaterial(surface)),
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
for (const river of water.rivers) {
|
|
243
|
+
const material = new MeshStandardMaterial({
|
|
244
|
+
color: RIVER,
|
|
245
|
+
roughness: 0.18,
|
|
246
|
+
metalness: 0,
|
|
247
|
+
transparent: true,
|
|
248
|
+
opacity: 0.9,
|
|
249
|
+
side: DoubleSide,
|
|
250
|
+
});
|
|
251
|
+
meshes.push(new Mesh(riverGeometry(river), material));
|
|
252
|
+
}
|
|
253
|
+
for (const mesh of meshes) ctx.add(mesh);
|
|
254
|
+
return {
|
|
255
|
+
lakes: water.lakes.length,
|
|
256
|
+
rivers: water.rivers.length,
|
|
257
|
+
dispose() {
|
|
258
|
+
for (const mesh of meshes) {
|
|
259
|
+
mesh.removeFromParent();
|
|
260
|
+
mesh.geometry.dispose();
|
|
261
|
+
(mesh.material as Material).dispose();
|
|
262
|
+
}
|
|
263
|
+
for (const surface of surfaces) surface.dispose();
|
|
264
|
+
},
|
|
265
|
+
};
|
|
266
|
+
}
|