@scenography/plugin-water 0.2.0
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/dist/data-BI-VQXjw.cjs +42 -0
- package/dist/data-BI-VQXjw.cjs.map +1 -0
- package/dist/data-CAd_goXP.mjs +31 -0
- package/dist/data-CAd_goXP.mjs.map +1 -0
- package/dist/data.cjs +4 -0
- package/dist/data.d.cts +2 -0
- package/dist/data.d.mts +2 -0
- package/dist/data.mjs +2 -0
- package/dist/index-BLdh03xr.d.cts +59 -0
- package/dist/index-BLdh03xr.d.mts +59 -0
- package/dist/index.cjs +244 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +35 -0
- package/dist/index.d.mts +35 -0
- package/dist/index.mjs +240 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +52 -0
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
let _scenography_scene = require("@scenography/scene");
|
|
2
|
+
//#region src/data/validate.ts
|
|
3
|
+
const validateWater = (water) => (0, _scenography_scene.validate)([
|
|
4
|
+
(0, _scenography_scene.existsProp)("size", "Water is missing a size."),
|
|
5
|
+
(0, _scenography_scene.propIs)("size", _scenography_scene.isVec2, "size must be [width, depth]."),
|
|
6
|
+
(0, _scenography_scene.propIs)("center", _scenography_scene.isVec2, "center must be [x, z]."),
|
|
7
|
+
(0, _scenography_scene.propIs)("level", _scenography_scene.isFiniteNumber, "level must be a finite number."),
|
|
8
|
+
...[
|
|
9
|
+
"waveHeight",
|
|
10
|
+
"waveScale",
|
|
11
|
+
"waveSpeed"
|
|
12
|
+
].map((key) => (0, _scenography_scene.propIs)(key, _scenography_scene.isNonNegativeNumber, `${key} must be a non-negative number.`)),
|
|
13
|
+
(0, _scenography_scene.propIs)("depthScale", _scenography_scene.isPositiveNumber, "depthScale must be a positive number."),
|
|
14
|
+
(0, _scenography_scene.propIs)("foam", _scenography_scene.isNonNegativeNumber, "foam must be a non-negative number.")
|
|
15
|
+
])(water);
|
|
16
|
+
//#endregion
|
|
17
|
+
//#region src/data/plugin.ts
|
|
18
|
+
/**
|
|
19
|
+
* The data half of the water plugin: owns the `water` scene key (a singleton)
|
|
20
|
+
* and validates it. Pure — no three.js; pass it to `defineScene` so scenes
|
|
21
|
+
* using water validate without loading the runtime.
|
|
22
|
+
*/
|
|
23
|
+
const waterData = {
|
|
24
|
+
kind: "water",
|
|
25
|
+
cardinality: "single",
|
|
26
|
+
validate: (water) => validateWater(water)
|
|
27
|
+
};
|
|
28
|
+
//#endregion
|
|
29
|
+
Object.defineProperty(exports, "validateWater", {
|
|
30
|
+
enumerable: true,
|
|
31
|
+
get: function() {
|
|
32
|
+
return validateWater;
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
Object.defineProperty(exports, "waterData", {
|
|
36
|
+
enumerable: true,
|
|
37
|
+
get: function() {
|
|
38
|
+
return waterData;
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
//# sourceMappingURL=data-BI-VQXjw.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"data-BI-VQXjw.cjs","names":["isVec2","isFiniteNumber","isNonNegativeNumber","isPositiveNumber"],"sources":["../src/data/validate.ts","../src/data/plugin.ts"],"sourcesContent":["import {\n existsProp,\n isFiniteNumber,\n isNonNegativeNumber,\n isPositiveNumber,\n isVec2,\n propIs,\n validate,\n type ValidationResult,\n} from \"@scenography/scene\";\n\nimport type { Water } from \"./water\";\n\nexport const validateWater = (water: Water): ValidationResult =>\n validate<Water>([\n existsProp(\"size\", \"Water is missing a size.\"),\n propIs(\"size\", isVec2, \"size must be [width, depth].\"),\n propIs(\"center\", isVec2, \"center must be [x, z].\"),\n propIs(\"level\", isFiniteNumber, \"level must be a finite number.\"),\n ...([\"waveHeight\", \"waveScale\", \"waveSpeed\"] as const).map((key) =>\n propIs(key, isNonNegativeNumber, `${key} must be a non-negative number.`),\n ),\n propIs(\"depthScale\", isPositiveNumber, \"depthScale must be a positive number.\"),\n propIs(\"foam\", isNonNegativeNumber, \"foam must be a non-negative number.\"),\n ])(water);\n","import type { DataPlugin } from \"@scenography/scene\";\n\nimport { validateWater } from \"./validate\";\nimport type { Water } from \"./water\";\n\n/**\n * The data half of the water plugin: owns the `water` scene key (a singleton)\n * and validates it. Pure — no three.js; pass it to `defineScene` so scenes\n * using water validate without loading the runtime.\n */\nexport const waterData: DataPlugin<Water> = {\n kind: \"water\",\n cardinality: \"single\",\n validate: (water) => validateWater(water),\n};\n"],"mappings":";;AAaA,MAAa,iBAAiB,WAAA,GAAA,mBAAA,SAAA,CACZ;oCACH,QAAQ,0BAA0B;gCACtC,QAAQA,mBAAAA,QAAQ,8BAA8B;gCAC9C,UAAUA,mBAAAA,QAAQ,wBAAwB;gCAC1C,SAASC,mBAAAA,gBAAgB,gCAAgC;CAChE,GAAI;EAAC;EAAc;EAAa;CAAW,CAAC,CAAW,KAAK,SAAA,GAAA,mBAAA,OAAA,CACnD,KAAKC,mBAAAA,qBAAqB,GAAG,IAAI,gCAAgC,CAC1E;gCACO,cAAcC,mBAAAA,kBAAkB,uCAAuC;gCACvE,QAAQD,mBAAAA,qBAAqB,qCAAqC;AAC3E,CAAC,CAAC,CAAC,KAAK;;;;;;;;ACdV,MAAa,YAA+B;CAC1C,MAAM;CACN,aAAa;CACb,WAAW,UAAU,cAAc,KAAK;AAC1C"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { existsProp, isFiniteNumber, isNonNegativeNumber, isPositiveNumber, isVec2, propIs, validate } from "@scenography/scene";
|
|
2
|
+
//#region src/data/validate.ts
|
|
3
|
+
const validateWater = (water) => validate([
|
|
4
|
+
existsProp("size", "Water is missing a size."),
|
|
5
|
+
propIs("size", isVec2, "size must be [width, depth]."),
|
|
6
|
+
propIs("center", isVec2, "center must be [x, z]."),
|
|
7
|
+
propIs("level", isFiniteNumber, "level must be a finite number."),
|
|
8
|
+
...[
|
|
9
|
+
"waveHeight",
|
|
10
|
+
"waveScale",
|
|
11
|
+
"waveSpeed"
|
|
12
|
+
].map((key) => propIs(key, isNonNegativeNumber, `${key} must be a non-negative number.`)),
|
|
13
|
+
propIs("depthScale", isPositiveNumber, "depthScale must be a positive number."),
|
|
14
|
+
propIs("foam", isNonNegativeNumber, "foam must be a non-negative number.")
|
|
15
|
+
])(water);
|
|
16
|
+
//#endregion
|
|
17
|
+
//#region src/data/plugin.ts
|
|
18
|
+
/**
|
|
19
|
+
* The data half of the water plugin: owns the `water` scene key (a singleton)
|
|
20
|
+
* and validates it. Pure — no three.js; pass it to `defineScene` so scenes
|
|
21
|
+
* using water validate without loading the runtime.
|
|
22
|
+
*/
|
|
23
|
+
const waterData = {
|
|
24
|
+
kind: "water",
|
|
25
|
+
cardinality: "single",
|
|
26
|
+
validate: (water) => validateWater(water)
|
|
27
|
+
};
|
|
28
|
+
//#endregion
|
|
29
|
+
export { validateWater as n, waterData as t };
|
|
30
|
+
|
|
31
|
+
//# sourceMappingURL=data-CAd_goXP.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"data-CAd_goXP.mjs","names":[],"sources":["../src/data/validate.ts","../src/data/plugin.ts"],"sourcesContent":["import {\n existsProp,\n isFiniteNumber,\n isNonNegativeNumber,\n isPositiveNumber,\n isVec2,\n propIs,\n validate,\n type ValidationResult,\n} from \"@scenography/scene\";\n\nimport type { Water } from \"./water\";\n\nexport const validateWater = (water: Water): ValidationResult =>\n validate<Water>([\n existsProp(\"size\", \"Water is missing a size.\"),\n propIs(\"size\", isVec2, \"size must be [width, depth].\"),\n propIs(\"center\", isVec2, \"center must be [x, z].\"),\n propIs(\"level\", isFiniteNumber, \"level must be a finite number.\"),\n ...([\"waveHeight\", \"waveScale\", \"waveSpeed\"] as const).map((key) =>\n propIs(key, isNonNegativeNumber, `${key} must be a non-negative number.`),\n ),\n propIs(\"depthScale\", isPositiveNumber, \"depthScale must be a positive number.\"),\n propIs(\"foam\", isNonNegativeNumber, \"foam must be a non-negative number.\"),\n ])(water);\n","import type { DataPlugin } from \"@scenography/scene\";\n\nimport { validateWater } from \"./validate\";\nimport type { Water } from \"./water\";\n\n/**\n * The data half of the water plugin: owns the `water` scene key (a singleton)\n * and validates it. Pure — no three.js; pass it to `defineScene` so scenes\n * using water validate without loading the runtime.\n */\nexport const waterData: DataPlugin<Water> = {\n kind: \"water\",\n cardinality: \"single\",\n validate: (water) => validateWater(water),\n};\n"],"mappings":";;AAaA,MAAa,iBAAiB,UAC5B,SAAgB;CACd,WAAW,QAAQ,0BAA0B;CAC7C,OAAO,QAAQ,QAAQ,8BAA8B;CACrD,OAAO,UAAU,QAAQ,wBAAwB;CACjD,OAAO,SAAS,gBAAgB,gCAAgC;CAChE,GAAI;EAAC;EAAc;EAAa;CAAW,CAAC,CAAW,KAAK,QAC1D,OAAO,KAAK,qBAAqB,GAAG,IAAI,gCAAgC,CAC1E;CACA,OAAO,cAAc,kBAAkB,uCAAuC;CAC9E,OAAO,QAAQ,qBAAqB,qCAAqC;AAC3E,CAAC,CAAC,CAAC,KAAK;;;;;;;;ACdV,MAAa,YAA+B;CAC1C,MAAM;CACN,aAAa;CACb,WAAW,UAAU,cAAc,KAAK;AAC1C"}
|
package/dist/data.cjs
ADDED
package/dist/data.d.cts
ADDED
package/dist/data.d.mts
ADDED
package/dist/data.mjs
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { DataPlugin, ValidationResult, Vec2 } from "@scenography/scene";
|
|
2
|
+
|
|
3
|
+
//#region src/data/water.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* A flat body of water — a transparent surface that refracts whatever opaque
|
|
6
|
+
* geometry sits below it (so the sea floor shows through). Pure data: the plane,
|
|
7
|
+
* its level, a color the water tints toward, and an optional ripple. The runtime
|
|
8
|
+
* builds it with a physically-based transmissive material.
|
|
9
|
+
*/
|
|
10
|
+
interface Water {
|
|
11
|
+
/** Plane extent `[width (x), depth (z)]` in world units. */
|
|
12
|
+
size: Vec2;
|
|
13
|
+
/** Center on the ground plane `[x, z]`. Defaults to `[0, 0]`. */
|
|
14
|
+
center?: Vec2;
|
|
15
|
+
/** Surface level (world Y). Defaults to `0`. */
|
|
16
|
+
level?: number;
|
|
17
|
+
/** Color the water tints toward (CSS). Defaults to a sea green-blue. */
|
|
18
|
+
color?: string;
|
|
19
|
+
/** Animate the surface with FBM waves (evolving normals, not a scrolling map). Defaults to `true`. */
|
|
20
|
+
ripple?: boolean;
|
|
21
|
+
/** Wave amplitude — how bumpy the surface normals get. Default `6`. */
|
|
22
|
+
waveHeight?: number;
|
|
23
|
+
/** Wave spatial frequency — higher is choppier/smaller waves. Default `0.02`. */
|
|
24
|
+
waveScale?: number;
|
|
25
|
+
/** Wave animation speed. Default `0.6`. */
|
|
26
|
+
waveSpeed?: number;
|
|
27
|
+
/**
|
|
28
|
+
* Deep-water tint (CSS). The surface leans toward it with depth (depth is read
|
|
29
|
+
* from the floor below — its base `y` plus heightmap relief). Defaults to a
|
|
30
|
+
* deep teal.
|
|
31
|
+
*/
|
|
32
|
+
depthColor?: string;
|
|
33
|
+
/** Depth (world units) over which color approaches `depthColor`. Default `100`. */
|
|
34
|
+
depthScale?: number;
|
|
35
|
+
/** Shoreline foam band width (world units of depth). `0` = none. Default `12`. */
|
|
36
|
+
foam?: number;
|
|
37
|
+
/** Foam color (CSS). Default white. */
|
|
38
|
+
foamColor?: string;
|
|
39
|
+
}
|
|
40
|
+
declare module "@scenography/scene" {
|
|
41
|
+
interface Scene {
|
|
42
|
+
/** A body of water (sea, pool…) — provided by `@scenography/plugin-water`. */
|
|
43
|
+
water?: Water;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
//#endregion
|
|
47
|
+
//#region src/data/validate.d.ts
|
|
48
|
+
declare const validateWater: (water: Water) => ValidationResult;
|
|
49
|
+
//#endregion
|
|
50
|
+
//#region src/data/plugin.d.ts
|
|
51
|
+
/**
|
|
52
|
+
* The data half of the water plugin: owns the `water` scene key (a singleton)
|
|
53
|
+
* and validates it. Pure — no three.js; pass it to `defineScene` so scenes
|
|
54
|
+
* using water validate without loading the runtime.
|
|
55
|
+
*/
|
|
56
|
+
declare const waterData: DataPlugin<Water>;
|
|
57
|
+
//#endregion
|
|
58
|
+
export { validateWater as n, Water as r, waterData as t };
|
|
59
|
+
//# sourceMappingURL=index-BLdh03xr.d.cts.map
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { DataPlugin, ValidationResult, Vec2 } from "@scenography/scene";
|
|
2
|
+
|
|
3
|
+
//#region src/data/water.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* A flat body of water — a transparent surface that refracts whatever opaque
|
|
6
|
+
* geometry sits below it (so the sea floor shows through). Pure data: the plane,
|
|
7
|
+
* its level, a color the water tints toward, and an optional ripple. The runtime
|
|
8
|
+
* builds it with a physically-based transmissive material.
|
|
9
|
+
*/
|
|
10
|
+
interface Water {
|
|
11
|
+
/** Plane extent `[width (x), depth (z)]` in world units. */
|
|
12
|
+
size: Vec2;
|
|
13
|
+
/** Center on the ground plane `[x, z]`. Defaults to `[0, 0]`. */
|
|
14
|
+
center?: Vec2;
|
|
15
|
+
/** Surface level (world Y). Defaults to `0`. */
|
|
16
|
+
level?: number;
|
|
17
|
+
/** Color the water tints toward (CSS). Defaults to a sea green-blue. */
|
|
18
|
+
color?: string;
|
|
19
|
+
/** Animate the surface with FBM waves (evolving normals, not a scrolling map). Defaults to `true`. */
|
|
20
|
+
ripple?: boolean;
|
|
21
|
+
/** Wave amplitude — how bumpy the surface normals get. Default `6`. */
|
|
22
|
+
waveHeight?: number;
|
|
23
|
+
/** Wave spatial frequency — higher is choppier/smaller waves. Default `0.02`. */
|
|
24
|
+
waveScale?: number;
|
|
25
|
+
/** Wave animation speed. Default `0.6`. */
|
|
26
|
+
waveSpeed?: number;
|
|
27
|
+
/**
|
|
28
|
+
* Deep-water tint (CSS). The surface leans toward it with depth (depth is read
|
|
29
|
+
* from the floor below — its base `y` plus heightmap relief). Defaults to a
|
|
30
|
+
* deep teal.
|
|
31
|
+
*/
|
|
32
|
+
depthColor?: string;
|
|
33
|
+
/** Depth (world units) over which color approaches `depthColor`. Default `100`. */
|
|
34
|
+
depthScale?: number;
|
|
35
|
+
/** Shoreline foam band width (world units of depth). `0` = none. Default `12`. */
|
|
36
|
+
foam?: number;
|
|
37
|
+
/** Foam color (CSS). Default white. */
|
|
38
|
+
foamColor?: string;
|
|
39
|
+
}
|
|
40
|
+
declare module "@scenography/scene" {
|
|
41
|
+
interface Scene {
|
|
42
|
+
/** A body of water (sea, pool…) — provided by `@scenography/plugin-water`. */
|
|
43
|
+
water?: Water;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
//#endregion
|
|
47
|
+
//#region src/data/validate.d.ts
|
|
48
|
+
declare const validateWater: (water: Water) => ValidationResult;
|
|
49
|
+
//#endregion
|
|
50
|
+
//#region src/data/plugin.d.ts
|
|
51
|
+
/**
|
|
52
|
+
* The data half of the water plugin: owns the `water` scene key (a singleton)
|
|
53
|
+
* and validates it. Pure — no three.js; pass it to `defineScene` so scenes
|
|
54
|
+
* using water validate without loading the runtime.
|
|
55
|
+
*/
|
|
56
|
+
declare const waterData: DataPlugin<Water>;
|
|
57
|
+
//#endregion
|
|
58
|
+
export { validateWater as n, Water as r, waterData as t };
|
|
59
|
+
//# sourceMappingURL=index-BLdh03xr.d.mts.map
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
const require_data = require("./data-BI-VQXjw.cjs");
|
|
3
|
+
let _scenography_engine = require("@scenography/engine");
|
|
4
|
+
let three = require("three");
|
|
5
|
+
//#region src/buildWater.ts
|
|
6
|
+
const WAVE_GLSL = `
|
|
7
|
+
varying vec3 vWaterWorld;
|
|
8
|
+
uniform float uTime, uWaterLevel, uFloorY, uFloorHeightScale, uDepthScale, uFoam;
|
|
9
|
+
uniform float uWaveHeight, uWaveScale, uWaveSpeed;
|
|
10
|
+
uniform vec2 uFloorCenter, uFloorSize;
|
|
11
|
+
uniform sampler2D uFloorHeightmap;
|
|
12
|
+
uniform vec3 uDepthColor, uFoamColor;
|
|
13
|
+
|
|
14
|
+
float wHash(vec2 p){ float h = dot(p, vec2(127.1, 311.7)); return fract(sin(h) * 43758.5453123); }
|
|
15
|
+
float wNoise(vec2 p){
|
|
16
|
+
vec2 i = floor(p), f = fract(p);
|
|
17
|
+
vec2 u = f * f * (3.0 - 2.0 * f);
|
|
18
|
+
return -1.0 + 2.0 * mix(mix(wHash(i + vec2(0.0,0.0)), wHash(i + vec2(1.0,0.0)), u.x),
|
|
19
|
+
mix(wHash(i + vec2(0.0,1.0)), wHash(i + vec2(1.0,1.0)), u.x), u.y);
|
|
20
|
+
}
|
|
21
|
+
float seaOctave(vec2 uv, float choppy){
|
|
22
|
+
uv += wNoise(uv);
|
|
23
|
+
vec2 wv = 1.0 - abs(sin(uv));
|
|
24
|
+
vec2 swv = abs(cos(uv));
|
|
25
|
+
wv = mix(wv, swv, wv);
|
|
26
|
+
return pow(1.0 - pow(wv.x * wv.y, 0.65), choppy);
|
|
27
|
+
}
|
|
28
|
+
float waveHeight(vec2 p){
|
|
29
|
+
float t = uTime * uWaveSpeed;
|
|
30
|
+
vec2 uv = p * uWaveScale; uv.x *= 0.75;
|
|
31
|
+
float freq = 1.0, amp = 1.0, choppy = 4.0, h = 0.0;
|
|
32
|
+
mat2 m = mat2(1.6, 1.2, -1.2, 1.6);
|
|
33
|
+
for(int i = 0; i < 3; i++){
|
|
34
|
+
float d = seaOctave((uv + t) * freq, choppy) + seaOctave((uv - t) * freq, choppy);
|
|
35
|
+
h += d * amp;
|
|
36
|
+
uv = m * uv; freq *= 1.9; amp *= 0.22; choppy = mix(choppy, 1.0, 0.2);
|
|
37
|
+
}
|
|
38
|
+
return h * uWaveHeight;
|
|
39
|
+
}
|
|
40
|
+
`;
|
|
41
|
+
/**
|
|
42
|
+
* Build a `Water` surface: a horizontal plane with a transmissive physical
|
|
43
|
+
* material (refracts the sea floor so the pebbles show through), upgraded with a
|
|
44
|
+
* custom shader that (1) ripples the surface normal with FBM waves that evolve in
|
|
45
|
+
* place, (2) tints by depth (the bottom is read from the `floor`'s base `y` plus
|
|
46
|
+
* its heightmap relief — no depth prepass), and (3) paints an irregular,
|
|
47
|
+
* advancing/receding shoreline foam line. Set
|
|
48
|
+
* `ripple: false` for a still, glassy surface.
|
|
49
|
+
*/
|
|
50
|
+
function buildWater(water, floor) {
|
|
51
|
+
const [width, depth] = water.size;
|
|
52
|
+
const [cx, cz] = water.center ?? [0, 0];
|
|
53
|
+
const level = water.level ?? 0;
|
|
54
|
+
const rippleOn = water.ripple !== false;
|
|
55
|
+
const material = new three.MeshPhysicalMaterial({
|
|
56
|
+
color: "#ffffff",
|
|
57
|
+
roughness: .12,
|
|
58
|
+
metalness: 0,
|
|
59
|
+
transmission: 1,
|
|
60
|
+
ior: 1.33,
|
|
61
|
+
thickness: 100,
|
|
62
|
+
attenuationColor: new three.Color(water.color ?? "#3b7d78"),
|
|
63
|
+
attenuationDistance: 400
|
|
64
|
+
});
|
|
65
|
+
const [fcx, fcz] = floor?.center ?? [0, 0];
|
|
66
|
+
const [fw, fd] = floor?.size ?? [1, 1];
|
|
67
|
+
const floorHeightScale = floor?.heightmap ? floor.heightScale ?? 0 : 0;
|
|
68
|
+
let bottomMap;
|
|
69
|
+
if (floor?.heightmap && floorHeightScale !== 0) bottomMap = new three.TextureLoader().load(floor.heightmap);
|
|
70
|
+
else {
|
|
71
|
+
bottomMap = new three.DataTexture(new Uint8Array([
|
|
72
|
+
0,
|
|
73
|
+
0,
|
|
74
|
+
0,
|
|
75
|
+
255
|
|
76
|
+
]), 1, 1);
|
|
77
|
+
bottomMap.needsUpdate = true;
|
|
78
|
+
}
|
|
79
|
+
const reflectionTarget = new three.WebGLRenderTarget(1, 1);
|
|
80
|
+
const uniforms = {
|
|
81
|
+
uTime: { value: 0 },
|
|
82
|
+
uWaterLevel: { value: level },
|
|
83
|
+
uFloorY: { value: floor?.y ?? 0 },
|
|
84
|
+
uFloorCenter: { value: new three.Vector2(fcx, fcz) },
|
|
85
|
+
uFloorSize: { value: new three.Vector2(fw, fd) },
|
|
86
|
+
uFloorHeightmap: { value: bottomMap },
|
|
87
|
+
uFloorHeightScale: { value: floorHeightScale },
|
|
88
|
+
uDepthColor: { value: new three.Color(water.depthColor ?? "#0d3b40") },
|
|
89
|
+
uDepthScale: { value: water.depthScale ?? 100 },
|
|
90
|
+
uFoam: { value: water.foam ?? 12 },
|
|
91
|
+
uFoamColor: { value: new three.Color(water.foamColor ?? "#eaf6f6") },
|
|
92
|
+
uWaveHeight: { value: rippleOn ? water.waveHeight ?? 6 : 0 },
|
|
93
|
+
uWaveScale: { value: water.waveScale ?? .02 },
|
|
94
|
+
uWaveSpeed: { value: water.waveSpeed ?? .6 },
|
|
95
|
+
uReflection: { value: reflectionTarget.texture },
|
|
96
|
+
uReflectionMatrix: { value: new three.Matrix4() }
|
|
97
|
+
};
|
|
98
|
+
material.onBeforeCompile = (shader) => {
|
|
99
|
+
Object.assign(shader.uniforms, uniforms);
|
|
100
|
+
shader.vertexShader = shader.vertexShader.replace("#include <common>", "#include <common>\nvarying vec3 vWaterWorld;\nvarying vec4 vReflectCoord;\nuniform mat4 uReflectionMatrix;").replace("#include <begin_vertex>", `#include <begin_vertex>
|
|
101
|
+
vWaterWorld = (modelMatrix * vec4(transformed, 1.0)).xyz;
|
|
102
|
+
vReflectCoord = uReflectionMatrix * vec4(transformed, 1.0);`);
|
|
103
|
+
shader.fragmentShader = shader.fragmentShader.replace("#include <common>", `#include <common>\n${WAVE_GLSL}\nvarying vec4 vReflectCoord;\nuniform sampler2D uReflection;\nvec2 gWaveSlope = vec2(0.0);`).replace("#include <normal_fragment_maps>", `#include <normal_fragment_maps>
|
|
104
|
+
if (uWaveHeight > 0.0) {
|
|
105
|
+
vec2 wp = vWaterWorld.xz;
|
|
106
|
+
float e = 2.0;
|
|
107
|
+
float hC = waveHeight(wp);
|
|
108
|
+
float dhdx = (waveHeight(wp + vec2(e, 0.0)) - hC) / e;
|
|
109
|
+
float dhdz = (waveHeight(wp + vec2(0.0, e)) - hC) / e;
|
|
110
|
+
vec3 wn = normalize(vec3(-dhdx, 1.0, -dhdz));
|
|
111
|
+
normal = normalize((viewMatrix * vec4(wn, 0.0)).xyz);
|
|
112
|
+
gWaveSlope = vec2(dhdx, dhdz); // fed to the reflection for ripples
|
|
113
|
+
}`).replace("#include <opaque_fragment>", `#include <opaque_fragment>
|
|
114
|
+
{
|
|
115
|
+
// Floor world (x, z) → heightmap UV. Matches the floor builder's
|
|
116
|
+
// mapping; v is flipped because three.js textures flip Y on upload.
|
|
117
|
+
vec2 huv = clamp((vWaterWorld.xz - uFloorCenter) / uFloorSize + 0.5, 0.0, 1.0);
|
|
118
|
+
float bottom = uFloorY + texture2D(uFloorHeightmap, huv).r * uFloorHeightScale;
|
|
119
|
+
float wdepth = max(uWaterLevel - bottom, 0.0);
|
|
120
|
+
float deep = 1.0 - exp(-wdepth / uDepthScale);
|
|
121
|
+
gl_FragColor.rgb = mix(gl_FragColor.rgb, uDepthColor, deep);
|
|
122
|
+
// Planar reflection: the mirrored scene (walls occlude the sky) sampled
|
|
123
|
+
// at this fragment's projected position, rippled by the wave slope and
|
|
124
|
+
// Fresnel-weighted so it dominates at grazing angles. This replaces the
|
|
125
|
+
// sky-only environment reflection, so the glint respects the walls.
|
|
126
|
+
vec2 rUv = vReflectCoord.xy / vReflectCoord.w + gWaveSlope * 0.06;
|
|
127
|
+
vec3 reflColor = texture2D(uReflection, rUv).rgb;
|
|
128
|
+
float fres = clamp(pow(1.0 - abs(dot(normalize(vViewPosition), normal)), 5.0), 0.0, 1.0);
|
|
129
|
+
gl_FragColor.rgb = mix(gl_FragColor.rgb, reflColor, mix(0.15, 0.95, fres));
|
|
130
|
+
float foam = 0.0;
|
|
131
|
+
if (uFoam > 0.0) {
|
|
132
|
+
// A thin lacy band hugging the waterline (depth 0..uFoam), not a wash
|
|
133
|
+
// across open water. A little noise wiggles its inner edge so the
|
|
134
|
+
// straight plane cut reads as an irregular shoreline; marching sine
|
|
135
|
+
// fronts add waves running up the shore.
|
|
136
|
+
float wiggle = wNoise(vWaterWorld.xz * 0.03 + uTime * 0.1) * 0.5 + 0.5;
|
|
137
|
+
float wd = max(wdepth + (wiggle - 0.5) * uFoam * 0.6, 0.0);
|
|
138
|
+
float band = 1.0 - smoothstep(0.0, uFoam, wd);
|
|
139
|
+
float lace = 0.5 + 0.5 * wNoise(vWaterWorld.xz * 0.1 - uTime * 0.25);
|
|
140
|
+
float fronts = 0.5 + 0.5 * sin(wd * 0.2 - uTime * 1.5);
|
|
141
|
+
foam = smoothstep(0.12, 0.5, band * band * lace * fronts);
|
|
142
|
+
gl_FragColor.rgb = mix(gl_FragColor.rgb, uFoamColor, foam);
|
|
143
|
+
}
|
|
144
|
+
gl_FragColor.a = max(gl_FragColor.a, max(deep * 0.7, foam));
|
|
145
|
+
}`);
|
|
146
|
+
};
|
|
147
|
+
material.customProgramCacheKey = () => "scenography-water";
|
|
148
|
+
const mesh = new three.Mesh(new three.PlaneGeometry(width, depth), material);
|
|
149
|
+
mesh.rotation.x = -Math.PI / 2;
|
|
150
|
+
mesh.position.set(cx, level, cz);
|
|
151
|
+
mesh.name = "water";
|
|
152
|
+
mesh.receiveShadow = true;
|
|
153
|
+
const update = (deltaMs) => {
|
|
154
|
+
uniforms.uTime.value += deltaMs / 1e3;
|
|
155
|
+
};
|
|
156
|
+
const reflectionCamera = new three.PerspectiveCamera();
|
|
157
|
+
const reflectorNormal = new three.Vector3(0, 1, 0);
|
|
158
|
+
const reflectorPos = new three.Vector3(cx, level, cz);
|
|
159
|
+
const reflectorPlane = new three.Plane();
|
|
160
|
+
const camPos = new three.Vector3();
|
|
161
|
+
const rot = new three.Matrix4();
|
|
162
|
+
const view = new three.Vector3();
|
|
163
|
+
const lookAt = new three.Vector3();
|
|
164
|
+
const target = new three.Vector3();
|
|
165
|
+
const clip = new three.Vector4();
|
|
166
|
+
const q = new three.Vector4();
|
|
167
|
+
const size = new three.Vector2();
|
|
168
|
+
const beforeRender = (renderer, scene, camera) => {
|
|
169
|
+
renderer.getDrawingBufferSize(size);
|
|
170
|
+
if (reflectionTarget.width !== size.x || reflectionTarget.height !== size.y) reflectionTarget.setSize(Math.max(1, size.x), Math.max(1, size.y));
|
|
171
|
+
camera.updateMatrixWorld();
|
|
172
|
+
mesh.updateMatrixWorld();
|
|
173
|
+
camPos.setFromMatrixPosition(camera.matrixWorld);
|
|
174
|
+
view.subVectors(reflectorPos, camPos);
|
|
175
|
+
if (view.dot(reflectorNormal) > 0) return;
|
|
176
|
+
view.reflect(reflectorNormal).negate().add(reflectorPos);
|
|
177
|
+
rot.extractRotation(camera.matrixWorld);
|
|
178
|
+
lookAt.set(0, 0, -1).applyMatrix4(rot).add(camPos);
|
|
179
|
+
target.subVectors(reflectorPos, lookAt).reflect(reflectorNormal).negate().add(reflectorPos);
|
|
180
|
+
reflectionCamera.position.copy(view);
|
|
181
|
+
reflectionCamera.up.set(0, 1, 0).applyMatrix4(rot).reflect(reflectorNormal);
|
|
182
|
+
reflectionCamera.lookAt(target);
|
|
183
|
+
reflectionCamera.far = camera.far;
|
|
184
|
+
reflectionCamera.updateMatrixWorld();
|
|
185
|
+
reflectionCamera.projectionMatrix.copy(camera.projectionMatrix);
|
|
186
|
+
const m = uniforms.uReflectionMatrix.value;
|
|
187
|
+
m.set(.5, 0, 0, .5, 0, .5, 0, .5, 0, 0, .5, .5, 0, 0, 0, 1);
|
|
188
|
+
m.multiply(reflectionCamera.projectionMatrix);
|
|
189
|
+
m.multiply(reflectionCamera.matrixWorldInverse);
|
|
190
|
+
m.multiply(mesh.matrixWorld);
|
|
191
|
+
reflectorPlane.setFromNormalAndCoplanarPoint(reflectorNormal, reflectorPos);
|
|
192
|
+
reflectorPlane.applyMatrix4(reflectionCamera.matrixWorldInverse);
|
|
193
|
+
clip.set(reflectorPlane.normal.x, reflectorPlane.normal.y, reflectorPlane.normal.z, reflectorPlane.constant);
|
|
194
|
+
const e = reflectionCamera.projectionMatrix.elements;
|
|
195
|
+
q.set((Math.sign(clip.x) + e[8]) / e[0], (Math.sign(clip.y) + e[9]) / e[5], -1, (1 + e[10]) / e[14]);
|
|
196
|
+
clip.multiplyScalar(2 / clip.dot(q));
|
|
197
|
+
e[2] = clip.x;
|
|
198
|
+
e[6] = clip.y;
|
|
199
|
+
e[10] = clip.z + 1;
|
|
200
|
+
e[14] = clip.w;
|
|
201
|
+
const prevTarget = renderer.getRenderTarget();
|
|
202
|
+
const prevShadowAuto = renderer.shadowMap.autoUpdate;
|
|
203
|
+
mesh.visible = false;
|
|
204
|
+
renderer.shadowMap.autoUpdate = false;
|
|
205
|
+
renderer.setRenderTarget(reflectionTarget);
|
|
206
|
+
renderer.clear();
|
|
207
|
+
renderer.render(scene, reflectionCamera);
|
|
208
|
+
renderer.setRenderTarget(prevTarget);
|
|
209
|
+
renderer.shadowMap.autoUpdate = prevShadowAuto;
|
|
210
|
+
mesh.visible = true;
|
|
211
|
+
};
|
|
212
|
+
const dispose = () => {
|
|
213
|
+
reflectionTarget.dispose();
|
|
214
|
+
};
|
|
215
|
+
return {
|
|
216
|
+
object: mesh,
|
|
217
|
+
update,
|
|
218
|
+
beforeRender,
|
|
219
|
+
dispose
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
//#endregion
|
|
223
|
+
//#region src/plugin.ts
|
|
224
|
+
/**
|
|
225
|
+
* The water plugin: materializes the `water` scene key as a transparent,
|
|
226
|
+
* depth-tinted surface with FBM waves and shoreline foam. Consumes the
|
|
227
|
+
* `terrain` capability (published by the floor) so the depth/foam shader can
|
|
228
|
+
* sample the same heightmap the ground is displaced by — and rebuilds when the
|
|
229
|
+
* floor changes (`dependsOn`).
|
|
230
|
+
*/
|
|
231
|
+
const water = () => ({
|
|
232
|
+
data: require_data.waterData,
|
|
233
|
+
dependsOn: [_scenography_engine.TERRAIN],
|
|
234
|
+
build: (definition, ctx) => {
|
|
235
|
+
return buildWater(definition, ctx.capabilities.get(_scenography_engine.TERRAIN)?.definition);
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
//#endregion
|
|
239
|
+
exports.buildWater = buildWater;
|
|
240
|
+
exports.validateWater = require_data.validateWater;
|
|
241
|
+
exports.water = water;
|
|
242
|
+
exports.waterData = require_data.waterData;
|
|
243
|
+
|
|
244
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.cjs","names":["MeshPhysicalMaterial","Color","TextureLoader","DataTexture","WebGLRenderTarget","Vector2","Matrix4","Mesh","PlaneGeometry","PerspectiveCamera","Vector3","Plane","Vector4","waterData","TERRAIN"],"sources":["../src/buildWater.ts","../src/plugin.ts"],"sourcesContent":["import {\n Color,\n DataTexture,\n Matrix4,\n Mesh,\n MeshPhysicalMaterial,\n PerspectiveCamera,\n Plane,\n PlaneGeometry,\n TextureLoader,\n Vector2,\n Vector3,\n Vector4,\n WebGLRenderTarget,\n type Object3D,\n type PerspectiveCamera as PerspectiveCameraType,\n type Scene as ThreeScene,\n type WebGLRenderer,\n type WebGLProgramParametersWithUniforms,\n} from \"three\";\nimport type { TerrainDefinition } from \"@scenography/engine\";\n\nimport type { Water } from \"./data\";\n\n/** A built water surface: the mesh plus its per-frame update and reflection pass. */\nexport interface BuiltWater {\n object: Object3D;\n update: (deltaMs: number) => void;\n beforeRender: (renderer: WebGLRenderer, scene: ThreeScene, camera: PerspectiveCameraType) => void;\n dispose: () => void;\n}\n\n// FBM wave + noise helpers, borrowed from \"Seascape\" by Alexander Alekseev (TDM,\n// CC BY-NC-SA 3.0): waves evolve in place (not a scrolling texture) so the calm\n// surface doesn't look like a repeating slide. Injected into the fragment shader.\nconst WAVE_GLSL = /* glsl */ `\n varying vec3 vWaterWorld;\n uniform float uTime, uWaterLevel, uFloorY, uFloorHeightScale, uDepthScale, uFoam;\n uniform float uWaveHeight, uWaveScale, uWaveSpeed;\n uniform vec2 uFloorCenter, uFloorSize;\n uniform sampler2D uFloorHeightmap;\n uniform vec3 uDepthColor, uFoamColor;\n\n float wHash(vec2 p){ float h = dot(p, vec2(127.1, 311.7)); return fract(sin(h) * 43758.5453123); }\n float wNoise(vec2 p){\n vec2 i = floor(p), f = fract(p);\n vec2 u = f * f * (3.0 - 2.0 * f);\n return -1.0 + 2.0 * mix(mix(wHash(i + vec2(0.0,0.0)), wHash(i + vec2(1.0,0.0)), u.x),\n mix(wHash(i + vec2(0.0,1.0)), wHash(i + vec2(1.0,1.0)), u.x), u.y);\n }\n float seaOctave(vec2 uv, float choppy){\n uv += wNoise(uv);\n vec2 wv = 1.0 - abs(sin(uv));\n vec2 swv = abs(cos(uv));\n wv = mix(wv, swv, wv);\n return pow(1.0 - pow(wv.x * wv.y, 0.65), choppy);\n }\n float waveHeight(vec2 p){\n float t = uTime * uWaveSpeed;\n vec2 uv = p * uWaveScale; uv.x *= 0.75;\n float freq = 1.0, amp = 1.0, choppy = 4.0, h = 0.0;\n mat2 m = mat2(1.6, 1.2, -1.2, 1.6);\n for(int i = 0; i < 3; i++){\n float d = seaOctave((uv + t) * freq, choppy) + seaOctave((uv - t) * freq, choppy);\n h += d * amp;\n uv = m * uv; freq *= 1.9; amp *= 0.22; choppy = mix(choppy, 1.0, 0.2);\n }\n return h * uWaveHeight;\n }\n`;\n\n/**\n * Build a `Water` surface: a horizontal plane with a transmissive physical\n * material (refracts the sea floor so the pebbles show through), upgraded with a\n * custom shader that (1) ripples the surface normal with FBM waves that evolve in\n * place, (2) tints by depth (the bottom is read from the `floor`'s base `y` plus\n * its heightmap relief — no depth prepass), and (3) paints an irregular,\n * advancing/receding shoreline foam line. Set\n * `ripple: false` for a still, glassy surface.\n */\nexport function buildWater(water: Water, floor?: TerrainDefinition): BuiltWater {\n const [width, depth] = water.size;\n const [cx, cz] = water.center ?? [0, 0];\n const level = water.level ?? 0;\n const rippleOn = water.ripple !== false;\n\n const material = new MeshPhysicalMaterial({\n color: \"#ffffff\",\n roughness: 0.12,\n metalness: 0,\n transmission: 1,\n ior: 1.33,\n thickness: 100,\n attenuationColor: new Color(water.color ?? \"#3b7d78\"),\n attenuationDistance: 400,\n });\n\n // The depth/foam shader samples the floor's heightmap to know the bottom under\n // each fragment (there is no depth prepass). A 1×1 black texture keeps the\n // sampler bound when the floor is flat (bottom = uFloorY everywhere).\n const [fcx, fcz] = floor?.center ?? [0, 0];\n const [fw, fd] = floor?.size ?? [1, 1];\n const floorHeightScale = floor?.heightmap ? floor.heightScale ?? 0 : 0;\n let bottomMap;\n if (floor?.heightmap && floorHeightScale !== 0) {\n bottomMap = new TextureLoader().load(floor.heightmap);\n } else {\n bottomMap = new DataTexture(new Uint8Array([0, 0, 0, 255]), 1, 1);\n bottomMap.needsUpdate = true;\n }\n // Planar reflection: the scene rendered mirrored across the water plane, so\n // walls (and everything else) occlude the sky in the reflection.\n const reflectionTarget = new WebGLRenderTarget(1, 1);\n const uniforms = {\n uTime: { value: 0 },\n uWaterLevel: { value: level },\n uFloorY: { value: floor?.y ?? 0 },\n uFloorCenter: { value: new Vector2(fcx, fcz) },\n uFloorSize: { value: new Vector2(fw, fd) },\n uFloorHeightmap: { value: bottomMap },\n uFloorHeightScale: { value: floorHeightScale },\n uDepthColor: { value: new Color(water.depthColor ?? \"#0d3b40\") },\n uDepthScale: { value: water.depthScale ?? 100 },\n uFoam: { value: water.foam ?? 12 },\n uFoamColor: { value: new Color(water.foamColor ?? \"#eaf6f6\") },\n uWaveHeight: { value: rippleOn ? water.waveHeight ?? 6 : 0 },\n uWaveScale: { value: water.waveScale ?? 0.02 },\n uWaveSpeed: { value: water.waveSpeed ?? 0.6 },\n uReflection: { value: reflectionTarget.texture },\n uReflectionMatrix: { value: new Matrix4() },\n };\n\n material.onBeforeCompile = (shader: WebGLProgramParametersWithUniforms): void => {\n Object.assign(shader.uniforms, uniforms);\n\n shader.vertexShader = shader.vertexShader\n .replace(\n \"#include <common>\",\n \"#include <common>\\nvarying vec3 vWaterWorld;\\nvarying vec4 vReflectCoord;\\nuniform mat4 uReflectionMatrix;\",\n )\n .replace(\n \"#include <begin_vertex>\",\n `#include <begin_vertex>\n vWaterWorld = (modelMatrix * vec4(transformed, 1.0)).xyz;\n vReflectCoord = uReflectionMatrix * vec4(transformed, 1.0);`,\n );\n\n shader.fragmentShader = shader.fragmentShader\n .replace(\n \"#include <common>\",\n `#include <common>\\n${WAVE_GLSL}\\nvarying vec4 vReflectCoord;\\nuniform sampler2D uReflection;\\nvec2 gWaveSlope = vec2(0.0);`,\n )\n // Replace the flat surface normal with the FBM wave normal (world → view),\n // so refraction and sun glints move organically. Runs after the map normals.\n .replace(\n \"#include <normal_fragment_maps>\",\n `#include <normal_fragment_maps>\n if (uWaveHeight > 0.0) {\n vec2 wp = vWaterWorld.xz;\n float e = 2.0;\n float hC = waveHeight(wp);\n float dhdx = (waveHeight(wp + vec2(e, 0.0)) - hC) / e;\n float dhdz = (waveHeight(wp + vec2(0.0, e)) - hC) / e;\n vec3 wn = normalize(vec3(-dhdx, 1.0, -dhdz));\n normal = normalize((viewMatrix * vec4(wn, 0.0)).xyz);\n gWaveSlope = vec2(dhdx, dhdz); // fed to the reflection for ripples\n }`,\n )\n // Depth tint + irregular, breathing shoreline foam, after the final color.\n .replace(\n \"#include <opaque_fragment>\",\n `#include <opaque_fragment>\n {\n // Floor world (x, z) → heightmap UV. Matches the floor builder's\n // mapping; v is flipped because three.js textures flip Y on upload.\n vec2 huv = clamp((vWaterWorld.xz - uFloorCenter) / uFloorSize + 0.5, 0.0, 1.0);\n float bottom = uFloorY + texture2D(uFloorHeightmap, huv).r * uFloorHeightScale;\n float wdepth = max(uWaterLevel - bottom, 0.0);\n float deep = 1.0 - exp(-wdepth / uDepthScale);\n gl_FragColor.rgb = mix(gl_FragColor.rgb, uDepthColor, deep);\n // Planar reflection: the mirrored scene (walls occlude the sky) sampled\n // at this fragment's projected position, rippled by the wave slope and\n // Fresnel-weighted so it dominates at grazing angles. This replaces the\n // sky-only environment reflection, so the glint respects the walls.\n vec2 rUv = vReflectCoord.xy / vReflectCoord.w + gWaveSlope * 0.06;\n vec3 reflColor = texture2D(uReflection, rUv).rgb;\n float fres = clamp(pow(1.0 - abs(dot(normalize(vViewPosition), normal)), 5.0), 0.0, 1.0);\n gl_FragColor.rgb = mix(gl_FragColor.rgb, reflColor, mix(0.15, 0.95, fres));\n float foam = 0.0;\n if (uFoam > 0.0) {\n // A thin lacy band hugging the waterline (depth 0..uFoam), not a wash\n // across open water. A little noise wiggles its inner edge so the\n // straight plane cut reads as an irregular shoreline; marching sine\n // fronts add waves running up the shore.\n float wiggle = wNoise(vWaterWorld.xz * 0.03 + uTime * 0.1) * 0.5 + 0.5;\n float wd = max(wdepth + (wiggle - 0.5) * uFoam * 0.6, 0.0);\n float band = 1.0 - smoothstep(0.0, uFoam, wd);\n float lace = 0.5 + 0.5 * wNoise(vWaterWorld.xz * 0.1 - uTime * 0.25);\n float fronts = 0.5 + 0.5 * sin(wd * 0.2 - uTime * 1.5);\n foam = smoothstep(0.12, 0.5, band * band * lace * fronts);\n gl_FragColor.rgb = mix(gl_FragColor.rgb, uFoamColor, foam);\n }\n gl_FragColor.a = max(gl_FragColor.a, max(deep * 0.7, foam));\n }`,\n );\n };\n // Keep this material's program separate from other physical materials'.\n material.customProgramCacheKey = (): string => \"scenography-water\";\n\n const mesh = new Mesh(new PlaneGeometry(width, depth), material);\n // PlaneGeometry faces +Z; lay it flat at the water level.\n mesh.rotation.x = -Math.PI / 2;\n mesh.position.set(cx, level, cz);\n mesh.name = \"water\";\n // Receive shadows so the sun's specular glint is cut where something (a wall)\n // casts a shadow onto the surface, instead of shining through it.\n mesh.receiveShadow = true;\n\n const update = (deltaMs: number): void => {\n uniforms.uTime.value += deltaMs / 1000;\n };\n\n // Reusable objects for the per-frame reflection pass (mirror the main camera\n // across the water plane, then render the scene into `reflectionTarget`).\n const reflectionCamera = new PerspectiveCamera();\n const reflectorNormal = new Vector3(0, 1, 0);\n const reflectorPos = new Vector3(cx, level, cz);\n const reflectorPlane = new Plane();\n const camPos = new Vector3();\n const rot = new Matrix4();\n const view = new Vector3();\n const lookAt = new Vector3();\n const target = new Vector3();\n const clip = new Vector4();\n const q = new Vector4();\n const size = new Vector2();\n\n const beforeRender = (\n renderer: WebGLRenderer,\n scene: ThreeScene,\n camera: PerspectiveCameraType,\n ): void => {\n renderer.getDrawingBufferSize(size);\n if (reflectionTarget.width !== size.x || reflectionTarget.height !== size.y) {\n reflectionTarget.setSize(Math.max(1, size.x), Math.max(1, size.y));\n }\n camera.updateMatrixWorld();\n mesh.updateMatrixWorld();\n camPos.setFromMatrixPosition(camera.matrixWorld);\n view.subVectors(reflectorPos, camPos);\n if (view.dot(reflectorNormal) > 0) return; // camera below the surface — skip\n view.reflect(reflectorNormal).negate().add(reflectorPos);\n rot.extractRotation(camera.matrixWorld);\n lookAt.set(0, 0, -1).applyMatrix4(rot).add(camPos);\n target.subVectors(reflectorPos, lookAt).reflect(reflectorNormal).negate().add(reflectorPos);\n reflectionCamera.position.copy(view);\n reflectionCamera.up.set(0, 1, 0).applyMatrix4(rot).reflect(reflectorNormal);\n reflectionCamera.lookAt(target);\n reflectionCamera.far = camera.far;\n reflectionCamera.updateMatrixWorld();\n reflectionCamera.projectionMatrix.copy(camera.projectionMatrix);\n\n // Texture matrix: bias · proj · viewInv · meshWorld → sample by fragment.\n const m = uniforms.uReflectionMatrix.value;\n m.set(0.5, 0, 0, 0.5, 0, 0.5, 0, 0.5, 0, 0, 0.5, 0.5, 0, 0, 0, 1);\n m.multiply(reflectionCamera.projectionMatrix);\n m.multiply(reflectionCamera.matrixWorldInverse);\n m.multiply(mesh.matrixWorld);\n\n // Oblique near-plane clip at the water surface, so geometry below it isn't\n // mirrored into the reflection (standard Reflector trick).\n reflectorPlane.setFromNormalAndCoplanarPoint(reflectorNormal, reflectorPos);\n reflectorPlane.applyMatrix4(reflectionCamera.matrixWorldInverse);\n clip.set(reflectorPlane.normal.x, reflectorPlane.normal.y, reflectorPlane.normal.z, reflectorPlane.constant);\n const e = reflectionCamera.projectionMatrix.elements;\n q.set((Math.sign(clip.x) + e[8]!) / e[0]!, (Math.sign(clip.y) + e[9]!) / e[5]!, -1, (1 + e[10]!) / e[14]!);\n clip.multiplyScalar(2 / clip.dot(q));\n e[2] = clip.x;\n e[6] = clip.y;\n e[10] = clip.z + 1;\n e[14] = clip.w;\n\n const prevTarget = renderer.getRenderTarget();\n const prevShadowAuto = renderer.shadowMap.autoUpdate;\n mesh.visible = false; // don't mirror the water into itself\n renderer.shadowMap.autoUpdate = false; // reuse the main shadow map\n renderer.setRenderTarget(reflectionTarget);\n renderer.clear();\n renderer.render(scene, reflectionCamera);\n renderer.setRenderTarget(prevTarget);\n renderer.shadowMap.autoUpdate = prevShadowAuto;\n mesh.visible = true;\n };\n\n const dispose = (): void => {\n reflectionTarget.dispose();\n };\n\n return { object: mesh, update, beforeRender, dispose };\n}\n","import { TERRAIN, type EnginePlugin, type TerrainCapability } from \"@scenography/engine\";\n\nimport { buildWater } from \"./buildWater\";\nimport { waterData } from \"./data\";\nimport type { Water } from \"./data\";\n\n/**\n * The water plugin: materializes the `water` scene key as a transparent,\n * depth-tinted surface with FBM waves and shoreline foam. Consumes the\n * `terrain` capability (published by the floor) so the depth/foam shader can\n * sample the same heightmap the ground is displaced by — and rebuilds when the\n * floor changes (`dependsOn`).\n */\nexport const water = (): EnginePlugin<Water> => ({\n data: waterData,\n dependsOn: [TERRAIN],\n build: (definition, ctx) => {\n const terrain = ctx.capabilities.get<TerrainCapability>(TERRAIN);\n return buildWater(definition, terrain?.definition);\n },\n});\n"],"mappings":"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"}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { n as validateWater, r as Water, t as waterData } from "./index-BLdh03xr.cjs";
|
|
2
|
+
import { EnginePlugin, TerrainDefinition } from "@scenography/engine";
|
|
3
|
+
import { Object3D, PerspectiveCamera, Scene, WebGLRenderer } from "three";
|
|
4
|
+
|
|
5
|
+
//#region src/plugin.d.ts
|
|
6
|
+
/**
|
|
7
|
+
* The water plugin: materializes the `water` scene key as a transparent,
|
|
8
|
+
* depth-tinted surface with FBM waves and shoreline foam. Consumes the
|
|
9
|
+
* `terrain` capability (published by the floor) so the depth/foam shader can
|
|
10
|
+
* sample the same heightmap the ground is displaced by — and rebuilds when the
|
|
11
|
+
* floor changes (`dependsOn`).
|
|
12
|
+
*/
|
|
13
|
+
declare const water: () => EnginePlugin<Water>;
|
|
14
|
+
//#endregion
|
|
15
|
+
//#region src/buildWater.d.ts
|
|
16
|
+
/** A built water surface: the mesh plus its per-frame update and reflection pass. */
|
|
17
|
+
interface BuiltWater {
|
|
18
|
+
object: Object3D;
|
|
19
|
+
update: (deltaMs: number) => void;
|
|
20
|
+
beforeRender: (renderer: WebGLRenderer, scene: Scene, camera: PerspectiveCamera) => void;
|
|
21
|
+
dispose: () => void;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Build a `Water` surface: a horizontal plane with a transmissive physical
|
|
25
|
+
* material (refracts the sea floor so the pebbles show through), upgraded with a
|
|
26
|
+
* custom shader that (1) ripples the surface normal with FBM waves that evolve in
|
|
27
|
+
* place, (2) tints by depth (the bottom is read from the `floor`'s base `y` plus
|
|
28
|
+
* its heightmap relief — no depth prepass), and (3) paints an irregular,
|
|
29
|
+
* advancing/receding shoreline foam line. Set
|
|
30
|
+
* `ripple: false` for a still, glassy surface.
|
|
31
|
+
*/
|
|
32
|
+
declare function buildWater(water: Water, floor?: TerrainDefinition): BuiltWater;
|
|
33
|
+
//#endregion
|
|
34
|
+
export { type BuiltWater, type Water, buildWater, validateWater, water, waterData };
|
|
35
|
+
//# sourceMappingURL=index.d.cts.map
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { n as validateWater, r as Water, t as waterData } from "./index-BLdh03xr.mjs";
|
|
2
|
+
import { EnginePlugin, TerrainDefinition } from "@scenography/engine";
|
|
3
|
+
import { Object3D, PerspectiveCamera, Scene, WebGLRenderer } from "three";
|
|
4
|
+
|
|
5
|
+
//#region src/plugin.d.ts
|
|
6
|
+
/**
|
|
7
|
+
* The water plugin: materializes the `water` scene key as a transparent,
|
|
8
|
+
* depth-tinted surface with FBM waves and shoreline foam. Consumes the
|
|
9
|
+
* `terrain` capability (published by the floor) so the depth/foam shader can
|
|
10
|
+
* sample the same heightmap the ground is displaced by — and rebuilds when the
|
|
11
|
+
* floor changes (`dependsOn`).
|
|
12
|
+
*/
|
|
13
|
+
declare const water: () => EnginePlugin<Water>;
|
|
14
|
+
//#endregion
|
|
15
|
+
//#region src/buildWater.d.ts
|
|
16
|
+
/** A built water surface: the mesh plus its per-frame update and reflection pass. */
|
|
17
|
+
interface BuiltWater {
|
|
18
|
+
object: Object3D;
|
|
19
|
+
update: (deltaMs: number) => void;
|
|
20
|
+
beforeRender: (renderer: WebGLRenderer, scene: Scene, camera: PerspectiveCamera) => void;
|
|
21
|
+
dispose: () => void;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Build a `Water` surface: a horizontal plane with a transmissive physical
|
|
25
|
+
* material (refracts the sea floor so the pebbles show through), upgraded with a
|
|
26
|
+
* custom shader that (1) ripples the surface normal with FBM waves that evolve in
|
|
27
|
+
* place, (2) tints by depth (the bottom is read from the `floor`'s base `y` plus
|
|
28
|
+
* its heightmap relief — no depth prepass), and (3) paints an irregular,
|
|
29
|
+
* advancing/receding shoreline foam line. Set
|
|
30
|
+
* `ripple: false` for a still, glassy surface.
|
|
31
|
+
*/
|
|
32
|
+
declare function buildWater(water: Water, floor?: TerrainDefinition): BuiltWater;
|
|
33
|
+
//#endregion
|
|
34
|
+
export { type BuiltWater, type Water, buildWater, validateWater, water, waterData };
|
|
35
|
+
//# sourceMappingURL=index.d.mts.map
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import { n as validateWater, t as waterData } from "./data-CAd_goXP.mjs";
|
|
2
|
+
import { TERRAIN } from "@scenography/engine";
|
|
3
|
+
import { Color, DataTexture, Matrix4, Mesh, MeshPhysicalMaterial, PerspectiveCamera, Plane, PlaneGeometry, TextureLoader, Vector2, Vector3, Vector4, WebGLRenderTarget } from "three";
|
|
4
|
+
//#region src/buildWater.ts
|
|
5
|
+
const WAVE_GLSL = `
|
|
6
|
+
varying vec3 vWaterWorld;
|
|
7
|
+
uniform float uTime, uWaterLevel, uFloorY, uFloorHeightScale, uDepthScale, uFoam;
|
|
8
|
+
uniform float uWaveHeight, uWaveScale, uWaveSpeed;
|
|
9
|
+
uniform vec2 uFloorCenter, uFloorSize;
|
|
10
|
+
uniform sampler2D uFloorHeightmap;
|
|
11
|
+
uniform vec3 uDepthColor, uFoamColor;
|
|
12
|
+
|
|
13
|
+
float wHash(vec2 p){ float h = dot(p, vec2(127.1, 311.7)); return fract(sin(h) * 43758.5453123); }
|
|
14
|
+
float wNoise(vec2 p){
|
|
15
|
+
vec2 i = floor(p), f = fract(p);
|
|
16
|
+
vec2 u = f * f * (3.0 - 2.0 * f);
|
|
17
|
+
return -1.0 + 2.0 * mix(mix(wHash(i + vec2(0.0,0.0)), wHash(i + vec2(1.0,0.0)), u.x),
|
|
18
|
+
mix(wHash(i + vec2(0.0,1.0)), wHash(i + vec2(1.0,1.0)), u.x), u.y);
|
|
19
|
+
}
|
|
20
|
+
float seaOctave(vec2 uv, float choppy){
|
|
21
|
+
uv += wNoise(uv);
|
|
22
|
+
vec2 wv = 1.0 - abs(sin(uv));
|
|
23
|
+
vec2 swv = abs(cos(uv));
|
|
24
|
+
wv = mix(wv, swv, wv);
|
|
25
|
+
return pow(1.0 - pow(wv.x * wv.y, 0.65), choppy);
|
|
26
|
+
}
|
|
27
|
+
float waveHeight(vec2 p){
|
|
28
|
+
float t = uTime * uWaveSpeed;
|
|
29
|
+
vec2 uv = p * uWaveScale; uv.x *= 0.75;
|
|
30
|
+
float freq = 1.0, amp = 1.0, choppy = 4.0, h = 0.0;
|
|
31
|
+
mat2 m = mat2(1.6, 1.2, -1.2, 1.6);
|
|
32
|
+
for(int i = 0; i < 3; i++){
|
|
33
|
+
float d = seaOctave((uv + t) * freq, choppy) + seaOctave((uv - t) * freq, choppy);
|
|
34
|
+
h += d * amp;
|
|
35
|
+
uv = m * uv; freq *= 1.9; amp *= 0.22; choppy = mix(choppy, 1.0, 0.2);
|
|
36
|
+
}
|
|
37
|
+
return h * uWaveHeight;
|
|
38
|
+
}
|
|
39
|
+
`;
|
|
40
|
+
/**
|
|
41
|
+
* Build a `Water` surface: a horizontal plane with a transmissive physical
|
|
42
|
+
* material (refracts the sea floor so the pebbles show through), upgraded with a
|
|
43
|
+
* custom shader that (1) ripples the surface normal with FBM waves that evolve in
|
|
44
|
+
* place, (2) tints by depth (the bottom is read from the `floor`'s base `y` plus
|
|
45
|
+
* its heightmap relief — no depth prepass), and (3) paints an irregular,
|
|
46
|
+
* advancing/receding shoreline foam line. Set
|
|
47
|
+
* `ripple: false` for a still, glassy surface.
|
|
48
|
+
*/
|
|
49
|
+
function buildWater(water, floor) {
|
|
50
|
+
const [width, depth] = water.size;
|
|
51
|
+
const [cx, cz] = water.center ?? [0, 0];
|
|
52
|
+
const level = water.level ?? 0;
|
|
53
|
+
const rippleOn = water.ripple !== false;
|
|
54
|
+
const material = new MeshPhysicalMaterial({
|
|
55
|
+
color: "#ffffff",
|
|
56
|
+
roughness: .12,
|
|
57
|
+
metalness: 0,
|
|
58
|
+
transmission: 1,
|
|
59
|
+
ior: 1.33,
|
|
60
|
+
thickness: 100,
|
|
61
|
+
attenuationColor: new Color(water.color ?? "#3b7d78"),
|
|
62
|
+
attenuationDistance: 400
|
|
63
|
+
});
|
|
64
|
+
const [fcx, fcz] = floor?.center ?? [0, 0];
|
|
65
|
+
const [fw, fd] = floor?.size ?? [1, 1];
|
|
66
|
+
const floorHeightScale = floor?.heightmap ? floor.heightScale ?? 0 : 0;
|
|
67
|
+
let bottomMap;
|
|
68
|
+
if (floor?.heightmap && floorHeightScale !== 0) bottomMap = new TextureLoader().load(floor.heightmap);
|
|
69
|
+
else {
|
|
70
|
+
bottomMap = new DataTexture(new Uint8Array([
|
|
71
|
+
0,
|
|
72
|
+
0,
|
|
73
|
+
0,
|
|
74
|
+
255
|
|
75
|
+
]), 1, 1);
|
|
76
|
+
bottomMap.needsUpdate = true;
|
|
77
|
+
}
|
|
78
|
+
const reflectionTarget = new WebGLRenderTarget(1, 1);
|
|
79
|
+
const uniforms = {
|
|
80
|
+
uTime: { value: 0 },
|
|
81
|
+
uWaterLevel: { value: level },
|
|
82
|
+
uFloorY: { value: floor?.y ?? 0 },
|
|
83
|
+
uFloorCenter: { value: new Vector2(fcx, fcz) },
|
|
84
|
+
uFloorSize: { value: new Vector2(fw, fd) },
|
|
85
|
+
uFloorHeightmap: { value: bottomMap },
|
|
86
|
+
uFloorHeightScale: { value: floorHeightScale },
|
|
87
|
+
uDepthColor: { value: new Color(water.depthColor ?? "#0d3b40") },
|
|
88
|
+
uDepthScale: { value: water.depthScale ?? 100 },
|
|
89
|
+
uFoam: { value: water.foam ?? 12 },
|
|
90
|
+
uFoamColor: { value: new Color(water.foamColor ?? "#eaf6f6") },
|
|
91
|
+
uWaveHeight: { value: rippleOn ? water.waveHeight ?? 6 : 0 },
|
|
92
|
+
uWaveScale: { value: water.waveScale ?? .02 },
|
|
93
|
+
uWaveSpeed: { value: water.waveSpeed ?? .6 },
|
|
94
|
+
uReflection: { value: reflectionTarget.texture },
|
|
95
|
+
uReflectionMatrix: { value: new Matrix4() }
|
|
96
|
+
};
|
|
97
|
+
material.onBeforeCompile = (shader) => {
|
|
98
|
+
Object.assign(shader.uniforms, uniforms);
|
|
99
|
+
shader.vertexShader = shader.vertexShader.replace("#include <common>", "#include <common>\nvarying vec3 vWaterWorld;\nvarying vec4 vReflectCoord;\nuniform mat4 uReflectionMatrix;").replace("#include <begin_vertex>", `#include <begin_vertex>
|
|
100
|
+
vWaterWorld = (modelMatrix * vec4(transformed, 1.0)).xyz;
|
|
101
|
+
vReflectCoord = uReflectionMatrix * vec4(transformed, 1.0);`);
|
|
102
|
+
shader.fragmentShader = shader.fragmentShader.replace("#include <common>", `#include <common>\n${WAVE_GLSL}\nvarying vec4 vReflectCoord;\nuniform sampler2D uReflection;\nvec2 gWaveSlope = vec2(0.0);`).replace("#include <normal_fragment_maps>", `#include <normal_fragment_maps>
|
|
103
|
+
if (uWaveHeight > 0.0) {
|
|
104
|
+
vec2 wp = vWaterWorld.xz;
|
|
105
|
+
float e = 2.0;
|
|
106
|
+
float hC = waveHeight(wp);
|
|
107
|
+
float dhdx = (waveHeight(wp + vec2(e, 0.0)) - hC) / e;
|
|
108
|
+
float dhdz = (waveHeight(wp + vec2(0.0, e)) - hC) / e;
|
|
109
|
+
vec3 wn = normalize(vec3(-dhdx, 1.0, -dhdz));
|
|
110
|
+
normal = normalize((viewMatrix * vec4(wn, 0.0)).xyz);
|
|
111
|
+
gWaveSlope = vec2(dhdx, dhdz); // fed to the reflection for ripples
|
|
112
|
+
}`).replace("#include <opaque_fragment>", `#include <opaque_fragment>
|
|
113
|
+
{
|
|
114
|
+
// Floor world (x, z) → heightmap UV. Matches the floor builder's
|
|
115
|
+
// mapping; v is flipped because three.js textures flip Y on upload.
|
|
116
|
+
vec2 huv = clamp((vWaterWorld.xz - uFloorCenter) / uFloorSize + 0.5, 0.0, 1.0);
|
|
117
|
+
float bottom = uFloorY + texture2D(uFloorHeightmap, huv).r * uFloorHeightScale;
|
|
118
|
+
float wdepth = max(uWaterLevel - bottom, 0.0);
|
|
119
|
+
float deep = 1.0 - exp(-wdepth / uDepthScale);
|
|
120
|
+
gl_FragColor.rgb = mix(gl_FragColor.rgb, uDepthColor, deep);
|
|
121
|
+
// Planar reflection: the mirrored scene (walls occlude the sky) sampled
|
|
122
|
+
// at this fragment's projected position, rippled by the wave slope and
|
|
123
|
+
// Fresnel-weighted so it dominates at grazing angles. This replaces the
|
|
124
|
+
// sky-only environment reflection, so the glint respects the walls.
|
|
125
|
+
vec2 rUv = vReflectCoord.xy / vReflectCoord.w + gWaveSlope * 0.06;
|
|
126
|
+
vec3 reflColor = texture2D(uReflection, rUv).rgb;
|
|
127
|
+
float fres = clamp(pow(1.0 - abs(dot(normalize(vViewPosition), normal)), 5.0), 0.0, 1.0);
|
|
128
|
+
gl_FragColor.rgb = mix(gl_FragColor.rgb, reflColor, mix(0.15, 0.95, fres));
|
|
129
|
+
float foam = 0.0;
|
|
130
|
+
if (uFoam > 0.0) {
|
|
131
|
+
// A thin lacy band hugging the waterline (depth 0..uFoam), not a wash
|
|
132
|
+
// across open water. A little noise wiggles its inner edge so the
|
|
133
|
+
// straight plane cut reads as an irregular shoreline; marching sine
|
|
134
|
+
// fronts add waves running up the shore.
|
|
135
|
+
float wiggle = wNoise(vWaterWorld.xz * 0.03 + uTime * 0.1) * 0.5 + 0.5;
|
|
136
|
+
float wd = max(wdepth + (wiggle - 0.5) * uFoam * 0.6, 0.0);
|
|
137
|
+
float band = 1.0 - smoothstep(0.0, uFoam, wd);
|
|
138
|
+
float lace = 0.5 + 0.5 * wNoise(vWaterWorld.xz * 0.1 - uTime * 0.25);
|
|
139
|
+
float fronts = 0.5 + 0.5 * sin(wd * 0.2 - uTime * 1.5);
|
|
140
|
+
foam = smoothstep(0.12, 0.5, band * band * lace * fronts);
|
|
141
|
+
gl_FragColor.rgb = mix(gl_FragColor.rgb, uFoamColor, foam);
|
|
142
|
+
}
|
|
143
|
+
gl_FragColor.a = max(gl_FragColor.a, max(deep * 0.7, foam));
|
|
144
|
+
}`);
|
|
145
|
+
};
|
|
146
|
+
material.customProgramCacheKey = () => "scenography-water";
|
|
147
|
+
const mesh = new Mesh(new PlaneGeometry(width, depth), material);
|
|
148
|
+
mesh.rotation.x = -Math.PI / 2;
|
|
149
|
+
mesh.position.set(cx, level, cz);
|
|
150
|
+
mesh.name = "water";
|
|
151
|
+
mesh.receiveShadow = true;
|
|
152
|
+
const update = (deltaMs) => {
|
|
153
|
+
uniforms.uTime.value += deltaMs / 1e3;
|
|
154
|
+
};
|
|
155
|
+
const reflectionCamera = new PerspectiveCamera();
|
|
156
|
+
const reflectorNormal = new Vector3(0, 1, 0);
|
|
157
|
+
const reflectorPos = new Vector3(cx, level, cz);
|
|
158
|
+
const reflectorPlane = new Plane();
|
|
159
|
+
const camPos = new Vector3();
|
|
160
|
+
const rot = new Matrix4();
|
|
161
|
+
const view = new Vector3();
|
|
162
|
+
const lookAt = new Vector3();
|
|
163
|
+
const target = new Vector3();
|
|
164
|
+
const clip = new Vector4();
|
|
165
|
+
const q = new Vector4();
|
|
166
|
+
const size = new Vector2();
|
|
167
|
+
const beforeRender = (renderer, scene, camera) => {
|
|
168
|
+
renderer.getDrawingBufferSize(size);
|
|
169
|
+
if (reflectionTarget.width !== size.x || reflectionTarget.height !== size.y) reflectionTarget.setSize(Math.max(1, size.x), Math.max(1, size.y));
|
|
170
|
+
camera.updateMatrixWorld();
|
|
171
|
+
mesh.updateMatrixWorld();
|
|
172
|
+
camPos.setFromMatrixPosition(camera.matrixWorld);
|
|
173
|
+
view.subVectors(reflectorPos, camPos);
|
|
174
|
+
if (view.dot(reflectorNormal) > 0) return;
|
|
175
|
+
view.reflect(reflectorNormal).negate().add(reflectorPos);
|
|
176
|
+
rot.extractRotation(camera.matrixWorld);
|
|
177
|
+
lookAt.set(0, 0, -1).applyMatrix4(rot).add(camPos);
|
|
178
|
+
target.subVectors(reflectorPos, lookAt).reflect(reflectorNormal).negate().add(reflectorPos);
|
|
179
|
+
reflectionCamera.position.copy(view);
|
|
180
|
+
reflectionCamera.up.set(0, 1, 0).applyMatrix4(rot).reflect(reflectorNormal);
|
|
181
|
+
reflectionCamera.lookAt(target);
|
|
182
|
+
reflectionCamera.far = camera.far;
|
|
183
|
+
reflectionCamera.updateMatrixWorld();
|
|
184
|
+
reflectionCamera.projectionMatrix.copy(camera.projectionMatrix);
|
|
185
|
+
const m = uniforms.uReflectionMatrix.value;
|
|
186
|
+
m.set(.5, 0, 0, .5, 0, .5, 0, .5, 0, 0, .5, .5, 0, 0, 0, 1);
|
|
187
|
+
m.multiply(reflectionCamera.projectionMatrix);
|
|
188
|
+
m.multiply(reflectionCamera.matrixWorldInverse);
|
|
189
|
+
m.multiply(mesh.matrixWorld);
|
|
190
|
+
reflectorPlane.setFromNormalAndCoplanarPoint(reflectorNormal, reflectorPos);
|
|
191
|
+
reflectorPlane.applyMatrix4(reflectionCamera.matrixWorldInverse);
|
|
192
|
+
clip.set(reflectorPlane.normal.x, reflectorPlane.normal.y, reflectorPlane.normal.z, reflectorPlane.constant);
|
|
193
|
+
const e = reflectionCamera.projectionMatrix.elements;
|
|
194
|
+
q.set((Math.sign(clip.x) + e[8]) / e[0], (Math.sign(clip.y) + e[9]) / e[5], -1, (1 + e[10]) / e[14]);
|
|
195
|
+
clip.multiplyScalar(2 / clip.dot(q));
|
|
196
|
+
e[2] = clip.x;
|
|
197
|
+
e[6] = clip.y;
|
|
198
|
+
e[10] = clip.z + 1;
|
|
199
|
+
e[14] = clip.w;
|
|
200
|
+
const prevTarget = renderer.getRenderTarget();
|
|
201
|
+
const prevShadowAuto = renderer.shadowMap.autoUpdate;
|
|
202
|
+
mesh.visible = false;
|
|
203
|
+
renderer.shadowMap.autoUpdate = false;
|
|
204
|
+
renderer.setRenderTarget(reflectionTarget);
|
|
205
|
+
renderer.clear();
|
|
206
|
+
renderer.render(scene, reflectionCamera);
|
|
207
|
+
renderer.setRenderTarget(prevTarget);
|
|
208
|
+
renderer.shadowMap.autoUpdate = prevShadowAuto;
|
|
209
|
+
mesh.visible = true;
|
|
210
|
+
};
|
|
211
|
+
const dispose = () => {
|
|
212
|
+
reflectionTarget.dispose();
|
|
213
|
+
};
|
|
214
|
+
return {
|
|
215
|
+
object: mesh,
|
|
216
|
+
update,
|
|
217
|
+
beforeRender,
|
|
218
|
+
dispose
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
//#endregion
|
|
222
|
+
//#region src/plugin.ts
|
|
223
|
+
/**
|
|
224
|
+
* The water plugin: materializes the `water` scene key as a transparent,
|
|
225
|
+
* depth-tinted surface with FBM waves and shoreline foam. Consumes the
|
|
226
|
+
* `terrain` capability (published by the floor) so the depth/foam shader can
|
|
227
|
+
* sample the same heightmap the ground is displaced by — and rebuilds when the
|
|
228
|
+
* floor changes (`dependsOn`).
|
|
229
|
+
*/
|
|
230
|
+
const water = () => ({
|
|
231
|
+
data: waterData,
|
|
232
|
+
dependsOn: [TERRAIN],
|
|
233
|
+
build: (definition, ctx) => {
|
|
234
|
+
return buildWater(definition, ctx.capabilities.get(TERRAIN)?.definition);
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
//#endregion
|
|
238
|
+
export { buildWater, validateWater, water, waterData };
|
|
239
|
+
|
|
240
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../src/buildWater.ts","../src/plugin.ts"],"sourcesContent":["import {\n Color,\n DataTexture,\n Matrix4,\n Mesh,\n MeshPhysicalMaterial,\n PerspectiveCamera,\n Plane,\n PlaneGeometry,\n TextureLoader,\n Vector2,\n Vector3,\n Vector4,\n WebGLRenderTarget,\n type Object3D,\n type PerspectiveCamera as PerspectiveCameraType,\n type Scene as ThreeScene,\n type WebGLRenderer,\n type WebGLProgramParametersWithUniforms,\n} from \"three\";\nimport type { TerrainDefinition } from \"@scenography/engine\";\n\nimport type { Water } from \"./data\";\n\n/** A built water surface: the mesh plus its per-frame update and reflection pass. */\nexport interface BuiltWater {\n object: Object3D;\n update: (deltaMs: number) => void;\n beforeRender: (renderer: WebGLRenderer, scene: ThreeScene, camera: PerspectiveCameraType) => void;\n dispose: () => void;\n}\n\n// FBM wave + noise helpers, borrowed from \"Seascape\" by Alexander Alekseev (TDM,\n// CC BY-NC-SA 3.0): waves evolve in place (not a scrolling texture) so the calm\n// surface doesn't look like a repeating slide. Injected into the fragment shader.\nconst WAVE_GLSL = /* glsl */ `\n varying vec3 vWaterWorld;\n uniform float uTime, uWaterLevel, uFloorY, uFloorHeightScale, uDepthScale, uFoam;\n uniform float uWaveHeight, uWaveScale, uWaveSpeed;\n uniform vec2 uFloorCenter, uFloorSize;\n uniform sampler2D uFloorHeightmap;\n uniform vec3 uDepthColor, uFoamColor;\n\n float wHash(vec2 p){ float h = dot(p, vec2(127.1, 311.7)); return fract(sin(h) * 43758.5453123); }\n float wNoise(vec2 p){\n vec2 i = floor(p), f = fract(p);\n vec2 u = f * f * (3.0 - 2.0 * f);\n return -1.0 + 2.0 * mix(mix(wHash(i + vec2(0.0,0.0)), wHash(i + vec2(1.0,0.0)), u.x),\n mix(wHash(i + vec2(0.0,1.0)), wHash(i + vec2(1.0,1.0)), u.x), u.y);\n }\n float seaOctave(vec2 uv, float choppy){\n uv += wNoise(uv);\n vec2 wv = 1.0 - abs(sin(uv));\n vec2 swv = abs(cos(uv));\n wv = mix(wv, swv, wv);\n return pow(1.0 - pow(wv.x * wv.y, 0.65), choppy);\n }\n float waveHeight(vec2 p){\n float t = uTime * uWaveSpeed;\n vec2 uv = p * uWaveScale; uv.x *= 0.75;\n float freq = 1.0, amp = 1.0, choppy = 4.0, h = 0.0;\n mat2 m = mat2(1.6, 1.2, -1.2, 1.6);\n for(int i = 0; i < 3; i++){\n float d = seaOctave((uv + t) * freq, choppy) + seaOctave((uv - t) * freq, choppy);\n h += d * amp;\n uv = m * uv; freq *= 1.9; amp *= 0.22; choppy = mix(choppy, 1.0, 0.2);\n }\n return h * uWaveHeight;\n }\n`;\n\n/**\n * Build a `Water` surface: a horizontal plane with a transmissive physical\n * material (refracts the sea floor so the pebbles show through), upgraded with a\n * custom shader that (1) ripples the surface normal with FBM waves that evolve in\n * place, (2) tints by depth (the bottom is read from the `floor`'s base `y` plus\n * its heightmap relief — no depth prepass), and (3) paints an irregular,\n * advancing/receding shoreline foam line. Set\n * `ripple: false` for a still, glassy surface.\n */\nexport function buildWater(water: Water, floor?: TerrainDefinition): BuiltWater {\n const [width, depth] = water.size;\n const [cx, cz] = water.center ?? [0, 0];\n const level = water.level ?? 0;\n const rippleOn = water.ripple !== false;\n\n const material = new MeshPhysicalMaterial({\n color: \"#ffffff\",\n roughness: 0.12,\n metalness: 0,\n transmission: 1,\n ior: 1.33,\n thickness: 100,\n attenuationColor: new Color(water.color ?? \"#3b7d78\"),\n attenuationDistance: 400,\n });\n\n // The depth/foam shader samples the floor's heightmap to know the bottom under\n // each fragment (there is no depth prepass). A 1×1 black texture keeps the\n // sampler bound when the floor is flat (bottom = uFloorY everywhere).\n const [fcx, fcz] = floor?.center ?? [0, 0];\n const [fw, fd] = floor?.size ?? [1, 1];\n const floorHeightScale = floor?.heightmap ? floor.heightScale ?? 0 : 0;\n let bottomMap;\n if (floor?.heightmap && floorHeightScale !== 0) {\n bottomMap = new TextureLoader().load(floor.heightmap);\n } else {\n bottomMap = new DataTexture(new Uint8Array([0, 0, 0, 255]), 1, 1);\n bottomMap.needsUpdate = true;\n }\n // Planar reflection: the scene rendered mirrored across the water plane, so\n // walls (and everything else) occlude the sky in the reflection.\n const reflectionTarget = new WebGLRenderTarget(1, 1);\n const uniforms = {\n uTime: { value: 0 },\n uWaterLevel: { value: level },\n uFloorY: { value: floor?.y ?? 0 },\n uFloorCenter: { value: new Vector2(fcx, fcz) },\n uFloorSize: { value: new Vector2(fw, fd) },\n uFloorHeightmap: { value: bottomMap },\n uFloorHeightScale: { value: floorHeightScale },\n uDepthColor: { value: new Color(water.depthColor ?? \"#0d3b40\") },\n uDepthScale: { value: water.depthScale ?? 100 },\n uFoam: { value: water.foam ?? 12 },\n uFoamColor: { value: new Color(water.foamColor ?? \"#eaf6f6\") },\n uWaveHeight: { value: rippleOn ? water.waveHeight ?? 6 : 0 },\n uWaveScale: { value: water.waveScale ?? 0.02 },\n uWaveSpeed: { value: water.waveSpeed ?? 0.6 },\n uReflection: { value: reflectionTarget.texture },\n uReflectionMatrix: { value: new Matrix4() },\n };\n\n material.onBeforeCompile = (shader: WebGLProgramParametersWithUniforms): void => {\n Object.assign(shader.uniforms, uniforms);\n\n shader.vertexShader = shader.vertexShader\n .replace(\n \"#include <common>\",\n \"#include <common>\\nvarying vec3 vWaterWorld;\\nvarying vec4 vReflectCoord;\\nuniform mat4 uReflectionMatrix;\",\n )\n .replace(\n \"#include <begin_vertex>\",\n `#include <begin_vertex>\n vWaterWorld = (modelMatrix * vec4(transformed, 1.0)).xyz;\n vReflectCoord = uReflectionMatrix * vec4(transformed, 1.0);`,\n );\n\n shader.fragmentShader = shader.fragmentShader\n .replace(\n \"#include <common>\",\n `#include <common>\\n${WAVE_GLSL}\\nvarying vec4 vReflectCoord;\\nuniform sampler2D uReflection;\\nvec2 gWaveSlope = vec2(0.0);`,\n )\n // Replace the flat surface normal with the FBM wave normal (world → view),\n // so refraction and sun glints move organically. Runs after the map normals.\n .replace(\n \"#include <normal_fragment_maps>\",\n `#include <normal_fragment_maps>\n if (uWaveHeight > 0.0) {\n vec2 wp = vWaterWorld.xz;\n float e = 2.0;\n float hC = waveHeight(wp);\n float dhdx = (waveHeight(wp + vec2(e, 0.0)) - hC) / e;\n float dhdz = (waveHeight(wp + vec2(0.0, e)) - hC) / e;\n vec3 wn = normalize(vec3(-dhdx, 1.0, -dhdz));\n normal = normalize((viewMatrix * vec4(wn, 0.0)).xyz);\n gWaveSlope = vec2(dhdx, dhdz); // fed to the reflection for ripples\n }`,\n )\n // Depth tint + irregular, breathing shoreline foam, after the final color.\n .replace(\n \"#include <opaque_fragment>\",\n `#include <opaque_fragment>\n {\n // Floor world (x, z) → heightmap UV. Matches the floor builder's\n // mapping; v is flipped because three.js textures flip Y on upload.\n vec2 huv = clamp((vWaterWorld.xz - uFloorCenter) / uFloorSize + 0.5, 0.0, 1.0);\n float bottom = uFloorY + texture2D(uFloorHeightmap, huv).r * uFloorHeightScale;\n float wdepth = max(uWaterLevel - bottom, 0.0);\n float deep = 1.0 - exp(-wdepth / uDepthScale);\n gl_FragColor.rgb = mix(gl_FragColor.rgb, uDepthColor, deep);\n // Planar reflection: the mirrored scene (walls occlude the sky) sampled\n // at this fragment's projected position, rippled by the wave slope and\n // Fresnel-weighted so it dominates at grazing angles. This replaces the\n // sky-only environment reflection, so the glint respects the walls.\n vec2 rUv = vReflectCoord.xy / vReflectCoord.w + gWaveSlope * 0.06;\n vec3 reflColor = texture2D(uReflection, rUv).rgb;\n float fres = clamp(pow(1.0 - abs(dot(normalize(vViewPosition), normal)), 5.0), 0.0, 1.0);\n gl_FragColor.rgb = mix(gl_FragColor.rgb, reflColor, mix(0.15, 0.95, fres));\n float foam = 0.0;\n if (uFoam > 0.0) {\n // A thin lacy band hugging the waterline (depth 0..uFoam), not a wash\n // across open water. A little noise wiggles its inner edge so the\n // straight plane cut reads as an irregular shoreline; marching sine\n // fronts add waves running up the shore.\n float wiggle = wNoise(vWaterWorld.xz * 0.03 + uTime * 0.1) * 0.5 + 0.5;\n float wd = max(wdepth + (wiggle - 0.5) * uFoam * 0.6, 0.0);\n float band = 1.0 - smoothstep(0.0, uFoam, wd);\n float lace = 0.5 + 0.5 * wNoise(vWaterWorld.xz * 0.1 - uTime * 0.25);\n float fronts = 0.5 + 0.5 * sin(wd * 0.2 - uTime * 1.5);\n foam = smoothstep(0.12, 0.5, band * band * lace * fronts);\n gl_FragColor.rgb = mix(gl_FragColor.rgb, uFoamColor, foam);\n }\n gl_FragColor.a = max(gl_FragColor.a, max(deep * 0.7, foam));\n }`,\n );\n };\n // Keep this material's program separate from other physical materials'.\n material.customProgramCacheKey = (): string => \"scenography-water\";\n\n const mesh = new Mesh(new PlaneGeometry(width, depth), material);\n // PlaneGeometry faces +Z; lay it flat at the water level.\n mesh.rotation.x = -Math.PI / 2;\n mesh.position.set(cx, level, cz);\n mesh.name = \"water\";\n // Receive shadows so the sun's specular glint is cut where something (a wall)\n // casts a shadow onto the surface, instead of shining through it.\n mesh.receiveShadow = true;\n\n const update = (deltaMs: number): void => {\n uniforms.uTime.value += deltaMs / 1000;\n };\n\n // Reusable objects for the per-frame reflection pass (mirror the main camera\n // across the water plane, then render the scene into `reflectionTarget`).\n const reflectionCamera = new PerspectiveCamera();\n const reflectorNormal = new Vector3(0, 1, 0);\n const reflectorPos = new Vector3(cx, level, cz);\n const reflectorPlane = new Plane();\n const camPos = new Vector3();\n const rot = new Matrix4();\n const view = new Vector3();\n const lookAt = new Vector3();\n const target = new Vector3();\n const clip = new Vector4();\n const q = new Vector4();\n const size = new Vector2();\n\n const beforeRender = (\n renderer: WebGLRenderer,\n scene: ThreeScene,\n camera: PerspectiveCameraType,\n ): void => {\n renderer.getDrawingBufferSize(size);\n if (reflectionTarget.width !== size.x || reflectionTarget.height !== size.y) {\n reflectionTarget.setSize(Math.max(1, size.x), Math.max(1, size.y));\n }\n camera.updateMatrixWorld();\n mesh.updateMatrixWorld();\n camPos.setFromMatrixPosition(camera.matrixWorld);\n view.subVectors(reflectorPos, camPos);\n if (view.dot(reflectorNormal) > 0) return; // camera below the surface — skip\n view.reflect(reflectorNormal).negate().add(reflectorPos);\n rot.extractRotation(camera.matrixWorld);\n lookAt.set(0, 0, -1).applyMatrix4(rot).add(camPos);\n target.subVectors(reflectorPos, lookAt).reflect(reflectorNormal).negate().add(reflectorPos);\n reflectionCamera.position.copy(view);\n reflectionCamera.up.set(0, 1, 0).applyMatrix4(rot).reflect(reflectorNormal);\n reflectionCamera.lookAt(target);\n reflectionCamera.far = camera.far;\n reflectionCamera.updateMatrixWorld();\n reflectionCamera.projectionMatrix.copy(camera.projectionMatrix);\n\n // Texture matrix: bias · proj · viewInv · meshWorld → sample by fragment.\n const m = uniforms.uReflectionMatrix.value;\n m.set(0.5, 0, 0, 0.5, 0, 0.5, 0, 0.5, 0, 0, 0.5, 0.5, 0, 0, 0, 1);\n m.multiply(reflectionCamera.projectionMatrix);\n m.multiply(reflectionCamera.matrixWorldInverse);\n m.multiply(mesh.matrixWorld);\n\n // Oblique near-plane clip at the water surface, so geometry below it isn't\n // mirrored into the reflection (standard Reflector trick).\n reflectorPlane.setFromNormalAndCoplanarPoint(reflectorNormal, reflectorPos);\n reflectorPlane.applyMatrix4(reflectionCamera.matrixWorldInverse);\n clip.set(reflectorPlane.normal.x, reflectorPlane.normal.y, reflectorPlane.normal.z, reflectorPlane.constant);\n const e = reflectionCamera.projectionMatrix.elements;\n q.set((Math.sign(clip.x) + e[8]!) / e[0]!, (Math.sign(clip.y) + e[9]!) / e[5]!, -1, (1 + e[10]!) / e[14]!);\n clip.multiplyScalar(2 / clip.dot(q));\n e[2] = clip.x;\n e[6] = clip.y;\n e[10] = clip.z + 1;\n e[14] = clip.w;\n\n const prevTarget = renderer.getRenderTarget();\n const prevShadowAuto = renderer.shadowMap.autoUpdate;\n mesh.visible = false; // don't mirror the water into itself\n renderer.shadowMap.autoUpdate = false; // reuse the main shadow map\n renderer.setRenderTarget(reflectionTarget);\n renderer.clear();\n renderer.render(scene, reflectionCamera);\n renderer.setRenderTarget(prevTarget);\n renderer.shadowMap.autoUpdate = prevShadowAuto;\n mesh.visible = true;\n };\n\n const dispose = (): void => {\n reflectionTarget.dispose();\n };\n\n return { object: mesh, update, beforeRender, dispose };\n}\n","import { TERRAIN, type EnginePlugin, type TerrainCapability } from \"@scenography/engine\";\n\nimport { buildWater } from \"./buildWater\";\nimport { waterData } from \"./data\";\nimport type { Water } from \"./data\";\n\n/**\n * The water plugin: materializes the `water` scene key as a transparent,\n * depth-tinted surface with FBM waves and shoreline foam. Consumes the\n * `terrain` capability (published by the floor) so the depth/foam shader can\n * sample the same heightmap the ground is displaced by — and rebuilds when the\n * floor changes (`dependsOn`).\n */\nexport const water = (): EnginePlugin<Water> => ({\n data: waterData,\n dependsOn: [TERRAIN],\n build: (definition, ctx) => {\n const terrain = ctx.capabilities.get<TerrainCapability>(TERRAIN);\n return buildWater(definition, terrain?.definition);\n },\n});\n"],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@scenography/plugin-water",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Water plugin for scenography: a transparent, depth-tinted sea/pool surface with FBM waves and shoreline foam. Data half importable without three.js.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"files": [
|
|
7
|
+
"dist"
|
|
8
|
+
],
|
|
9
|
+
"type": "module",
|
|
10
|
+
"main": "./dist/index.cjs",
|
|
11
|
+
"module": "./dist/index.mjs",
|
|
12
|
+
"types": "./dist/index.d.cts",
|
|
13
|
+
"exports": {
|
|
14
|
+
".": {
|
|
15
|
+
"import": {
|
|
16
|
+
"types": "./dist/index.d.mts",
|
|
17
|
+
"default": "./dist/index.mjs"
|
|
18
|
+
},
|
|
19
|
+
"require": {
|
|
20
|
+
"types": "./dist/index.d.cts",
|
|
21
|
+
"default": "./dist/index.cjs"
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"./data": {
|
|
25
|
+
"import": {
|
|
26
|
+
"types": "./dist/data.d.mts",
|
|
27
|
+
"default": "./dist/data.mjs"
|
|
28
|
+
},
|
|
29
|
+
"require": {
|
|
30
|
+
"types": "./dist/data.d.cts",
|
|
31
|
+
"default": "./dist/data.cjs"
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"@scenography/scene": "0.2.0"
|
|
37
|
+
},
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"@types/three": "^0.160.0",
|
|
40
|
+
"three": "^0.160.0",
|
|
41
|
+
"@scenography/engine": "0.2.0"
|
|
42
|
+
},
|
|
43
|
+
"peerDependencies": {
|
|
44
|
+
"three": ">=0.160.0",
|
|
45
|
+
"@scenography/engine": "0.2.0"
|
|
46
|
+
},
|
|
47
|
+
"scripts": {
|
|
48
|
+
"build": "tsdown",
|
|
49
|
+
"dev": "tsdown --watch",
|
|
50
|
+
"typecheck": "tsc --noEmit"
|
|
51
|
+
}
|
|
52
|
+
}
|