@jgengine/shell 0.25.0 → 0.27.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/CHANGELOG.md +109 -0
- package/dist/GamePlayerShell.d.ts +8 -0
- package/dist/GamePlayerShell.js +15 -1
- package/dist/Shell3dPresentation.js +29 -11
- package/dist/devtools/FrameTelemetry.d.ts +12 -0
- package/dist/devtools/FrameTelemetry.js +68 -0
- package/dist/devtools/agentBridge.js +2 -0
- package/dist/devtools/devtoolsReports.d.ts +2 -0
- package/dist/devtools/devtoolsReports.js +3 -0
- package/dist/devtools/frameSampler.d.ts +127 -0
- package/dist/devtools/frameSampler.js +345 -0
- package/dist/environment/AuthoredDaylight.d.ts +28 -0
- package/dist/environment/AuthoredDaylight.js +42 -0
- package/dist/environment/Daylight.d.ts +55 -7
- package/dist/environment/Daylight.js +126 -29
- package/dist/environment/EnvironmentScene.js +51 -8
- package/dist/environment/daylightCycle.d.ts +26 -4
- package/dist/environment/daylightCycle.js +36 -5
- package/dist/postfx/PostProcessing.d.ts +7 -1
- package/dist/postfx/PostProcessing.js +28 -2
- package/dist/postfx/autoExposure.d.ts +49 -0
- package/dist/postfx/autoExposure.js +189 -0
- package/dist/postfx/gradeShader.js +43 -4
- package/dist/presentationRecovery3d.js +2 -1
- package/dist/render/CascadedShadows.d.ts +3 -1
- package/dist/render/CascadedShadows.js +9 -1
- package/dist/render/EnvironmentLighting.d.ts +10 -1
- package/dist/render/EnvironmentLighting.js +11 -3
- package/dist/render/IrradianceVolumeLighting.d.ts +18 -0
- package/dist/render/IrradianceVolumeLighting.js +35 -0
- package/dist/render/SceneLighting.d.ts +14 -1
- package/dist/render/SceneLighting.js +29 -8
- package/dist/render/authoredGrade.d.ts +11 -0
- package/dist/render/authoredGrade.js +26 -0
- package/dist/render/decalGeometry.d.ts +26 -0
- package/dist/render/decalGeometry.js +54 -0
- package/dist/render/decalTexture.d.ts +13 -0
- package/dist/render/decalTexture.js +114 -0
- package/dist/render/modelWind.d.ts +28 -0
- package/dist/render/modelWind.js +103 -0
- package/dist/render/presentationLook.d.ts +15 -0
- package/dist/render/presentationLook.js +18 -0
- package/dist/render/stableTextureSet.d.ts +9 -0
- package/dist/render/stableTextureSet.js +24 -0
- package/dist/render/triplanarMaterial.d.ts +52 -0
- package/dist/render/triplanarMaterial.js +169 -0
- package/dist/render/useModelInstance.d.ts +1 -1
- package/dist/render/useModelInstance.js +3 -1
- package/dist/scatter/scatterModels.js +2 -0
- package/dist/scene/DecalRenderer.d.ts +2 -0
- package/dist/scene/DecalRenderer.js +74 -0
- package/dist/scene/GrassFieldRenderer.js +3 -2
- package/dist/scene/builtinSceneKindRenderers.js +2 -0
- package/dist/settings/appliedSettings.js +16 -13
- package/dist/terrain/GrassCover.d.ts +21 -0
- package/dist/terrain/GrassCover.js +84 -0
- package/dist/terrain/grassCoverGeometry.d.ts +40 -0
- package/dist/terrain/grassCoverGeometry.js +112 -0
- package/dist/terrain/terrainDetailMaterial.d.ts +12 -0
- package/dist/terrain/terrainDetailMaterial.js +54 -1
- package/dist/terrain/terrainSurfaces.d.ts +6 -0
- package/dist/terrain/terrainSurfaces.js +10 -0
- package/dist/world/WorldScene.js +2 -1
- package/package.json +4 -4
- package/skills/jgengine/api.md +59 -6
- package/skills/jgengine/capabilities.md +4 -0
- package/skills/jgengine-ui/api.md +114 -23
- package/skills/jgengine-ui/capabilities.md +14 -0
- package/skills/jgengine-ui/reference-primitives.md +4 -2
- package/skills/jgengine-world/SKILL.md +7 -1
- package/skills/jgengine-world/api.md +222 -8
- package/skills/jgengine-world/capabilities.md +42 -2
- package/skills/jgengine-world/recipes/npc-senses.md +8 -2
- package/skills/jgengine-world/recipes/vehicle-feel.md +5 -0
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useLayoutEffect, useRef } from "react";
|
|
3
|
+
import { useFrame } from "@react-three/fiber";
|
|
3
4
|
import { CascadedShadows, DEFAULT_SHADOW_RADIUS } from "./CascadedShadows.js";
|
|
4
5
|
const DEFAULT_BACKDROP_FOG_COLOR = "#1a1c22";
|
|
5
6
|
/** Default cap for combined dynamic point and spot lights. */
|
|
@@ -26,12 +27,12 @@ function resolveDynamicLights(lighting) {
|
|
|
26
27
|
function usesCascades(entry) {
|
|
27
28
|
return (entry.castShadow ?? false) && (entry.cascades ?? 1) > 1;
|
|
28
29
|
}
|
|
29
|
-
function DirectionalShadowLight({ entry, profile }) {
|
|
30
|
+
function DirectionalShadowLight({ entry, profile, modulation }) {
|
|
30
31
|
const resolved = profile === undefined ? entry : { ...entry, shadowMapSize: entry.shadowMapSize ?? profile.shadowMapSize, cascades: entry.cascades ?? profile.cascades };
|
|
31
32
|
if (usesCascades(resolved)) {
|
|
32
|
-
return _jsx(CascadedShadows, { entry: resolved });
|
|
33
|
+
return _jsx(CascadedShadows, { entry: resolved, modulation: modulation });
|
|
33
34
|
}
|
|
34
|
-
return _jsx(SingleShadowLight, { entry: resolved });
|
|
35
|
+
return _jsx(SingleShadowLight, { entry: resolved, modulation: modulation });
|
|
35
36
|
}
|
|
36
37
|
/**
|
|
37
38
|
* Refresh a directional light's shadow projection after its frustum bounds change.
|
|
@@ -42,11 +43,19 @@ function DirectionalShadowLight({ entry, profile }) {
|
|
|
42
43
|
export function syncShadowProjection(light) {
|
|
43
44
|
light.shadow.camera.updateProjectionMatrix();
|
|
44
45
|
}
|
|
45
|
-
function SingleShadowLight({ entry }) {
|
|
46
|
+
function SingleShadowLight({ entry, modulation }) {
|
|
46
47
|
const lightRef = useRef(null);
|
|
47
48
|
const size = entry.shadowCameraSize ?? 40;
|
|
48
49
|
const far = Math.max(200, size * 6);
|
|
49
50
|
const mapSize = entry.shadowMapSize ?? 1024;
|
|
51
|
+
const baseIntensity = entry.intensity ?? 1.3;
|
|
52
|
+
useFrame(() => {
|
|
53
|
+
const light = lightRef.current;
|
|
54
|
+
if (light === null || modulation === undefined)
|
|
55
|
+
return;
|
|
56
|
+
light.intensity = baseIntensity * modulation.sunFactor;
|
|
57
|
+
light.color.set(modulation.sunColor);
|
|
58
|
+
});
|
|
50
59
|
useLayoutEffect(() => {
|
|
51
60
|
if (lightRef.current !== null)
|
|
52
61
|
syncShadowProjection(lightRef.current);
|
|
@@ -60,13 +69,25 @@ function SingleShadowLight({ entry }) {
|
|
|
60
69
|
}, [mapSize]);
|
|
61
70
|
return (_jsx("directionalLight", { ref: lightRef, position: [entry.position[0], entry.position[1], entry.position[2]], intensity: entry.intensity ?? 1.3, color: entry.color, castShadow: entry.castShadow ?? false, "shadow-mapSize-width": mapSize, "shadow-mapSize-height": mapSize, "shadow-camera-left": -size, "shadow-camera-right": size, "shadow-camera-top": size, "shadow-camera-bottom": -size, "shadow-camera-near": 0.5, "shadow-camera-far": far, "shadow-bias": entry.shadowBias ?? -0.0004, "shadow-normalBias": entry.shadowNormalBias ?? 0.02, "shadow-radius": entry.shadowRadius ?? DEFAULT_SHADOW_RADIUS }));
|
|
62
71
|
}
|
|
63
|
-
export function ConfiguredLighting({ lighting, profile }) {
|
|
72
|
+
export function ConfiguredLighting({ lighting, profile, modulation, }) {
|
|
64
73
|
const dynamic = resolveDynamicLights(lighting);
|
|
65
|
-
|
|
74
|
+
const ambientRef = useRef(null);
|
|
75
|
+
const hemisphereRef = useRef(null);
|
|
76
|
+
const baseAmbient = lighting.ambient?.intensity ?? 0.55;
|
|
77
|
+
const baseHemisphere = lighting.hemisphere?.intensity ?? 0.55;
|
|
78
|
+
useFrame(() => {
|
|
79
|
+
if (modulation === undefined)
|
|
80
|
+
return;
|
|
81
|
+
if (ambientRef.current !== null)
|
|
82
|
+
ambientRef.current.intensity = baseAmbient * modulation.ambientFactor;
|
|
83
|
+
if (hemisphereRef.current !== null)
|
|
84
|
+
hemisphereRef.current.intensity = baseHemisphere * modulation.ambientFactor;
|
|
85
|
+
});
|
|
86
|
+
return (_jsxs(_Fragment, { children: [lighting.ambient !== undefined ? (_jsx("ambientLight", { ref: ambientRef, color: lighting.ambient.color, intensity: baseAmbient })) : null, lighting.hemisphere !== undefined ? (_jsx("hemisphereLight", { ref: hemisphereRef, args: [
|
|
66
87
|
lighting.hemisphere.skyColor ?? "#bfe3ff",
|
|
67
88
|
lighting.hemisphere.groundColor ?? "#4c6b34",
|
|
68
|
-
|
|
69
|
-
] })) : null, (lighting.directional ?? []).map((entry, index) => (_jsx(DirectionalShadowLight, { entry: entry, profile: profile }, index))), dynamic.point.map((entry, index) => (_jsx("pointLight", { position: [entry.position[0], entry.position[1], entry.position[2]], intensity: entry.intensity ?? 1, color: entry.color, distance: entry.distance ?? 0, decay: entry.decay ?? 2, castShadow: entry.castShadow ?? false }, index))), dynamic.spot.map((entry, index) => (_jsx("spotLight", { position: [entry.position[0], entry.position[1], entry.position[2]], intensity: entry.intensity ?? 1, color: entry.color, distance: entry.distance ?? 0, decay: entry.decay ?? 2, angle: entry.angle ?? Math.PI / 6, penumbra: entry.penumbra ?? 0, castShadow: entry.castShadow ?? false, "target-position": [...(entry.target ?? [0, 0, 0])] }, index)))] }));
|
|
89
|
+
baseHemisphere,
|
|
90
|
+
] })) : null, (lighting.directional ?? []).map((entry, index) => (_jsx(DirectionalShadowLight, { entry: entry, profile: profile, ...(index === 0 && modulation !== undefined ? { modulation } : {}) }, index))), dynamic.point.map((entry, index) => (_jsx("pointLight", { position: [entry.position[0], entry.position[1], entry.position[2]], intensity: entry.intensity ?? 1, color: entry.color, distance: entry.distance ?? 0, decay: entry.decay ?? 2, castShadow: entry.castShadow ?? false }, index))), dynamic.spot.map((entry, index) => (_jsx("spotLight", { position: [entry.position[0], entry.position[1], entry.position[2]], intensity: entry.intensity ?? 1, color: entry.color, distance: entry.distance ?? 0, decay: entry.decay ?? 2, angle: entry.angle ?? Math.PI / 6, penumbra: entry.penumbra ?? 0, castShadow: entry.castShadow ?? false, "target-position": [...(entry.target ?? [0, 0, 0])] }, index)))] }));
|
|
70
91
|
}
|
|
71
92
|
export function BackdropFog({ fog }) {
|
|
72
93
|
if (fog === undefined)
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { DocumentColorGrade } from "@jgengine/core/editor/environment";
|
|
2
|
+
import type { ResolvedGameLook } from "@jgengine/core/render/lookPreset";
|
|
3
|
+
/**
|
|
4
|
+
* Folds a scene document's authored grade into the resolved look. The document is the authoring
|
|
5
|
+
* source, so its baked LUT and exposure range win over the game's static post config, and everything
|
|
6
|
+
* the document left unauthored stays exactly as the game or look preset set it. With no authored
|
|
7
|
+
* grade the look comes back untouched (same identity), so an ungraded game mounts the pipeline it
|
|
8
|
+
* always mounted.
|
|
9
|
+
* @internal
|
|
10
|
+
*/
|
|
11
|
+
export declare function withAuthoredGrade(look: ResolvedGameLook, grade: DocumentColorGrade | undefined): ResolvedGameLook;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Folds a scene document's authored grade into the resolved look. The document is the authoring
|
|
3
|
+
* source, so its baked LUT and exposure range win over the game's static post config, and everything
|
|
4
|
+
* the document left unauthored stays exactly as the game or look preset set it. With no authored
|
|
5
|
+
* grade the look comes back untouched (same identity), so an ungraded game mounts the pipeline it
|
|
6
|
+
* always mounted.
|
|
7
|
+
* @internal
|
|
8
|
+
*/
|
|
9
|
+
export function withAuthoredGrade(look, grade) {
|
|
10
|
+
if (grade === undefined)
|
|
11
|
+
return look;
|
|
12
|
+
const post = look.postProcessing ?? { toneMapping: "agx", bloom: false };
|
|
13
|
+
const graded = grade.lut === undefined
|
|
14
|
+
? post
|
|
15
|
+
: { ...post, grade: { ...(typeof post.grade === "object" ? post.grade : {}), lut: { cube: grade.lut } } };
|
|
16
|
+
if (grade.autoExposure === undefined) {
|
|
17
|
+
return { ...look, postProcessing: graded };
|
|
18
|
+
}
|
|
19
|
+
return {
|
|
20
|
+
...look,
|
|
21
|
+
postProcessing: {
|
|
22
|
+
...graded,
|
|
23
|
+
autoExposure: { ...(typeof graded.autoExposure === "object" ? graded.autoExposure : {}), ...grade.autoExposure },
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { TerrainHeightSampler } from "../terrain/terrainMath.js";
|
|
2
|
+
/** Inputs for {@link createDecalDrapeGeometry}. */
|
|
3
|
+
export interface DecalDrapeOptions {
|
|
4
|
+
/** Decal edge length in metres. */
|
|
5
|
+
size: number;
|
|
6
|
+
rotationY: number;
|
|
7
|
+
heightAt: TerrainHeightSampler;
|
|
8
|
+
/** Height above the sampled ground, metres. */
|
|
9
|
+
lift: number;
|
|
10
|
+
/** Grid resolution per axis; defaults to 12 (bounded, decals are small). */
|
|
11
|
+
segments?: number;
|
|
12
|
+
}
|
|
13
|
+
/** Baked vertex/index buffers for one draped ground decal. */
|
|
14
|
+
export interface DecalDrapeGeometry {
|
|
15
|
+
positions: Float32Array;
|
|
16
|
+
normals: Float32Array;
|
|
17
|
+
uvs: Float32Array;
|
|
18
|
+
indices: Uint32Array;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* A ground decal's draped quad: an `size`² patch rotated by `rotationY`, every vertex lifted onto
|
|
22
|
+
* the live terrain so a stain follows a curb or a camber instead of floating over it or sinking
|
|
23
|
+
* through it. Bounded to one small grid — decals are metres, not patches.
|
|
24
|
+
* @internal
|
|
25
|
+
*/
|
|
26
|
+
export declare function createDecalDrapeGeometry(options: DecalDrapeOptions): DecalDrapeGeometry;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A ground decal's draped quad: an `size`² patch rotated by `rotationY`, every vertex lifted onto
|
|
3
|
+
* the live terrain so a stain follows a curb or a camber instead of floating over it or sinking
|
|
4
|
+
* through it. Bounded to one small grid — decals are metres, not patches.
|
|
5
|
+
* @internal
|
|
6
|
+
*/
|
|
7
|
+
export function createDecalDrapeGeometry(options) {
|
|
8
|
+
const segments = Math.max(2, Math.min(24, Math.floor(options.segments ?? 12)));
|
|
9
|
+
const half = options.size / 2;
|
|
10
|
+
const cos = Math.cos(options.rotationY);
|
|
11
|
+
const sin = Math.sin(options.rotationY);
|
|
12
|
+
const cols = segments + 1;
|
|
13
|
+
const positions = new Float32Array(cols * cols * 3);
|
|
14
|
+
const normals = new Float32Array(cols * cols * 3);
|
|
15
|
+
const uvs = new Float32Array(cols * cols * 2);
|
|
16
|
+
const eps = Math.max(0.05, options.size / segments / 2);
|
|
17
|
+
for (let iz = 0; iz < cols; iz += 1) {
|
|
18
|
+
for (let ix = 0; ix < cols; ix += 1) {
|
|
19
|
+
const lx = -half + (ix / segments) * options.size;
|
|
20
|
+
const lz = -half + (iz / segments) * options.size;
|
|
21
|
+
const x = lx * cos + lz * sin;
|
|
22
|
+
const z = -lx * sin + lz * cos;
|
|
23
|
+
const index = iz * cols + ix;
|
|
24
|
+
positions[index * 3] = x;
|
|
25
|
+
positions[index * 3 + 1] = options.heightAt(x, z) + options.lift;
|
|
26
|
+
positions[index * 3 + 2] = z;
|
|
27
|
+
const hx = options.heightAt(x + eps, z) - options.heightAt(x - eps, z);
|
|
28
|
+
const hz = options.heightAt(x, z + eps) - options.heightAt(x, z - eps);
|
|
29
|
+
const length = Math.hypot(hx, 2 * eps, hz) || 1;
|
|
30
|
+
normals[index * 3] = -hx / length;
|
|
31
|
+
normals[index * 3 + 1] = (2 * eps) / length;
|
|
32
|
+
normals[index * 3 + 2] = -hz / length;
|
|
33
|
+
uvs[index * 2] = ix / segments;
|
|
34
|
+
uvs[index * 2 + 1] = 1 - iz / segments;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
const indices = new Uint32Array(segments * segments * 6);
|
|
38
|
+
let cursor = 0;
|
|
39
|
+
for (let iz = 0; iz < segments; iz += 1) {
|
|
40
|
+
for (let ix = 0; ix < segments; ix += 1) {
|
|
41
|
+
const a = iz * cols + ix;
|
|
42
|
+
const b = a + 1;
|
|
43
|
+
const c = a + cols;
|
|
44
|
+
const d = c + 1;
|
|
45
|
+
indices[cursor++] = a;
|
|
46
|
+
indices[cursor++] = c;
|
|
47
|
+
indices[cursor++] = b;
|
|
48
|
+
indices[cursor++] = b;
|
|
49
|
+
indices[cursor++] = c;
|
|
50
|
+
indices[cursor++] = d;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return { positions, normals, uvs, indices };
|
|
54
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { DecalStyle } from "@jgengine/core/world/decalKind";
|
|
2
|
+
/** Row-major RGBA bytes plus their edge length, ready for a `THREE.DataTexture`. */
|
|
3
|
+
export interface DecalTextureData {
|
|
4
|
+
size: number;
|
|
5
|
+
data: Uint8Array;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* The deterministic procedural texture behind one decal style. Same style+seed always bakes the
|
|
9
|
+
* same bytes; the renderer uploads it once as a `DataTexture`, so no scan is pulled and no game
|
|
10
|
+
* ships stain art it cannot inspect. Alpha is the coverage; colour is the stain itself.
|
|
11
|
+
* @internal
|
|
12
|
+
*/
|
|
13
|
+
export declare function createDecalTexture(style: DecalStyle, seed: string, size?: number): DecalTextureData;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
function hash(x, y, seed) {
|
|
2
|
+
let h = Math.imul(x | 0, 374761393) ^ Math.imul(y | 0, 668265263) ^ Math.imul(seed | 0, 2246822519);
|
|
3
|
+
h = Math.imul(h ^ (h >>> 13), 1274126177);
|
|
4
|
+
return ((h ^ (h >>> 16)) >>> 0) / 4294967296;
|
|
5
|
+
}
|
|
6
|
+
function smooth(t) {
|
|
7
|
+
return t * t * (3 - 2 * t);
|
|
8
|
+
}
|
|
9
|
+
function noise(x, y, seed) {
|
|
10
|
+
const xi = Math.floor(x);
|
|
11
|
+
const yi = Math.floor(y);
|
|
12
|
+
const tx = smooth(x - xi);
|
|
13
|
+
const ty = smooth(y - yi);
|
|
14
|
+
const a = hash(xi, yi, seed);
|
|
15
|
+
const b = hash(xi + 1, yi, seed);
|
|
16
|
+
const c = hash(xi, yi + 1, seed);
|
|
17
|
+
const d = hash(xi + 1, yi + 1, seed);
|
|
18
|
+
return (a + (b - a) * tx) * (1 - ty) + (c + (d - c) * tx) * ty;
|
|
19
|
+
}
|
|
20
|
+
function fbm(x, y, seed) {
|
|
21
|
+
return noise(x, y, seed) * 0.62 + noise(x * 2.1 + 7.3, y * 2.1 - 3.1, seed + 11) * 0.26 + noise(x * 4.3 - 1.7, y * 4.3 + 5.9, seed + 23) * 0.12;
|
|
22
|
+
}
|
|
23
|
+
function seedOf(text) {
|
|
24
|
+
let h = 2166136261;
|
|
25
|
+
for (let i = 0; i < text.length; i += 1) {
|
|
26
|
+
h ^= text.charCodeAt(i);
|
|
27
|
+
h = Math.imul(h, 16777619);
|
|
28
|
+
}
|
|
29
|
+
return h >>> 0;
|
|
30
|
+
}
|
|
31
|
+
function smoothstep(edge0, edge1, x) {
|
|
32
|
+
const t = Math.min(1, Math.max(0, (x - edge0) / (edge1 - edge0 || 1e-6)));
|
|
33
|
+
return t * t * (3 - 2 * t);
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* The deterministic procedural texture behind one decal style. Same style+seed always bakes the
|
|
37
|
+
* same bytes; the renderer uploads it once as a `DataTexture`, so no scan is pulled and no game
|
|
38
|
+
* ships stain art it cannot inspect. Alpha is the coverage; colour is the stain itself.
|
|
39
|
+
* @internal
|
|
40
|
+
*/
|
|
41
|
+
export function createDecalTexture(style, seed, size = 128) {
|
|
42
|
+
const seedNumber = seedOf(seed.length > 0 ? seed : style);
|
|
43
|
+
const data = new Uint8Array(size * size * 4);
|
|
44
|
+
for (let y = 0; y < size; y += 1) {
|
|
45
|
+
for (let x = 0; x < size; x += 1) {
|
|
46
|
+
const u = x / size - 0.5;
|
|
47
|
+
const v = y / size - 0.5;
|
|
48
|
+
const r = Math.hypot(u, v) * 2;
|
|
49
|
+
const n = fbm(u * 5 + 13.1, v * 5 - 7.7, seedNumber);
|
|
50
|
+
let alpha = 0;
|
|
51
|
+
let red = 0;
|
|
52
|
+
let green = 0;
|
|
53
|
+
let blue = 0;
|
|
54
|
+
if (style === "dirt") {
|
|
55
|
+
const grain = fbm(u * 17 + 3.3, v * 17 + 9.1, seedNumber + 5);
|
|
56
|
+
alpha = smoothstep(0.5, 0.62, n) * (1 - smoothstep(0.55, 1, r)) * (0.55 + grain * 0.45);
|
|
57
|
+
const shade = 0.75 + grain * 0.5;
|
|
58
|
+
red = 0.36 * shade;
|
|
59
|
+
green = 0.29 * shade;
|
|
60
|
+
blue = 0.19 * shade;
|
|
61
|
+
}
|
|
62
|
+
else if (style === "blood") {
|
|
63
|
+
const splatter = fbm(u * 7 - 2.2, v * 7 + 4.4, seedNumber);
|
|
64
|
+
const main = smoothstep(0.46, 0.58, splatter) * (1 - smoothstep(0.6, 1, r));
|
|
65
|
+
// Satellite droplets: 26 seeded points, each a soft dark ring.
|
|
66
|
+
let drops = 0;
|
|
67
|
+
for (let i = 0; i < 26; i += 1) {
|
|
68
|
+
const a = hash(i, 3, seedNumber) * Math.PI * 2;
|
|
69
|
+
const d = 0.08 + hash(i, 7, seedNumber) * 0.42;
|
|
70
|
+
const radius = 0.012 + hash(i, 11, seedNumber) * 0.03;
|
|
71
|
+
const dist = Math.hypot(u - Math.cos(a) * d, v - Math.sin(a) * d);
|
|
72
|
+
drops = Math.max(drops, 1 - smoothstep(radius * 0.6, radius, dist));
|
|
73
|
+
}
|
|
74
|
+
alpha = Math.max(main, drops);
|
|
75
|
+
const shade = 0.8 + splatter * 0.35;
|
|
76
|
+
red = 0.46 * shade;
|
|
77
|
+
green = 0.055 * shade;
|
|
78
|
+
blue = 0.045 * shade;
|
|
79
|
+
}
|
|
80
|
+
else if (style === "paint") {
|
|
81
|
+
const body = smoothstep(0.44, 0.55, n) * (1 - smoothstep(0.7, 1, r));
|
|
82
|
+
const streak = smoothstep(0.38, 0.52, fbm(u * 22 + 1.7, v * 5 + 8.3, seedNumber + 3)) * smoothstep(0.2, 0.9, v + 0.5) * 0.5;
|
|
83
|
+
alpha = Math.max(body, streak * (1 - smoothstep(0.5, 0.9, r)));
|
|
84
|
+
const shade = 0.86 + n * 0.22;
|
|
85
|
+
red = 0.84 * shade;
|
|
86
|
+
green = 0.8 * shade;
|
|
87
|
+
blue = 0.72 * shade;
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
// Cracks: six warped radial lines plus two branches, faded toward the rim.
|
|
91
|
+
let line = 0;
|
|
92
|
+
for (let i = 0; i < 8; i += 1) {
|
|
93
|
+
const base = (i / 8) * Math.PI * 2 + hash(i, 1, seedNumber) * 0.7;
|
|
94
|
+
const wobble = (fbm(u * 9 + i * 3.1, v * 9 - i * 2.3, seedNumber + i) - 0.5) * 0.5;
|
|
95
|
+
const angle = Math.atan2(v, u);
|
|
96
|
+
const delta = Math.abs(Math.atan2(Math.sin(angle - base - wobble), Math.cos(angle - base - wobble)));
|
|
97
|
+
const width = 0.022 + hash(i, 5, seedNumber) * 0.02;
|
|
98
|
+
line = Math.max(line, (1 - smoothstep(0, width, delta * Math.max(0.05, r))) * (1 - smoothstep(0.45, 1, r)));
|
|
99
|
+
}
|
|
100
|
+
const grit = fbm(u * 30 + 4.1, v * 30 - 2.8, seedNumber + 9) * 0.35;
|
|
101
|
+
alpha = Math.max(line, grit * smoothstep(0.35, 0.7, n) * (1 - smoothstep(0.6, 1, r)));
|
|
102
|
+
red = 0.13;
|
|
103
|
+
green = 0.12;
|
|
104
|
+
blue = 0.11;
|
|
105
|
+
}
|
|
106
|
+
const index = (y * size + x) * 4;
|
|
107
|
+
data[index] = Math.round(Math.min(1, red) * 255);
|
|
108
|
+
data[index + 1] = Math.round(Math.min(1, green) * 255);
|
|
109
|
+
data[index + 2] = Math.round(Math.min(1, blue) * 255);
|
|
110
|
+
data[index + 3] = Math.round(Math.min(1, alpha) * 255);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return { size, data };
|
|
114
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import * as THREE from "three";
|
|
2
|
+
import type { ModelWindConfig } from "@jgengine/core/game/playableGame";
|
|
3
|
+
/** Shared gust clock every foliage material reads; advanced once per frame by `WindClock`. */
|
|
4
|
+
export declare const windClock: {
|
|
5
|
+
value: number;
|
|
6
|
+
};
|
|
7
|
+
/** Shared gust direction (XZ); a model may override it with its own authored direction. */
|
|
8
|
+
export declare const windDirection: THREE.Vector2;
|
|
9
|
+
/** Resolved sway settings baked into one material. */
|
|
10
|
+
export interface ResolvedModelWind {
|
|
11
|
+
strength: number;
|
|
12
|
+
speed: number;
|
|
13
|
+
height: number;
|
|
14
|
+
direction: THREE.Vector2;
|
|
15
|
+
}
|
|
16
|
+
/** Fully-defaulted sway settings for a model. @internal */
|
|
17
|
+
export declare function resolveModelWind(wind: ModelWindConfig | false | undefined, fallbackHeight: number): ResolvedModelWind | null;
|
|
18
|
+
/**
|
|
19
|
+
* Compiles height-ramped wind sway into a standard material's vertex shader. Idempotent per
|
|
20
|
+
* material; the clock and direction are shared module uniforms, so the whole scene advances with
|
|
21
|
+
* one write per frame and no per-material bookkeeping.
|
|
22
|
+
* @capability model-wind sway a foliage model in the shared gust field
|
|
23
|
+
*/
|
|
24
|
+
export declare function applyModelWind(material: THREE.MeshStandardMaterial, wind: ResolvedModelWind): THREE.MeshStandardMaterial;
|
|
25
|
+
/** Applies wind to every standard material under a cloned model. @internal */
|
|
26
|
+
export declare function applyModelWindToScene(root: THREE.Object3D, wind: ModelWindConfig | false | undefined, fallbackHeight?: number): void;
|
|
27
|
+
/** Advances the shared gust clock once per frame; mount one per 3D world. */
|
|
28
|
+
export declare function WindClock(): null;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import * as THREE from "three";
|
|
2
|
+
import { useFrame } from "@react-three/fiber";
|
|
3
|
+
/** Shared gust clock every foliage material reads; advanced once per frame by `WindClock`. */
|
|
4
|
+
export const windClock = { value: 0 };
|
|
5
|
+
/** Shared gust direction (XZ); a model may override it with its own authored direction. */
|
|
6
|
+
export const windDirection = new THREE.Vector2(1, 0.35).normalize();
|
|
7
|
+
const WIND_KEY = "jgWind";
|
|
8
|
+
/** Fully-defaulted sway settings for a model. @internal */
|
|
9
|
+
export function resolveModelWind(wind, fallbackHeight) {
|
|
10
|
+
if (wind === false || wind === undefined)
|
|
11
|
+
return null;
|
|
12
|
+
const strength = wind.strength ?? 0.35;
|
|
13
|
+
if (!Number.isFinite(strength) || strength < 0)
|
|
14
|
+
throw new Error("Model wind strength must be finite and non-negative");
|
|
15
|
+
const speed = wind.speed ?? 1;
|
|
16
|
+
if (!Number.isFinite(speed) || speed <= 0)
|
|
17
|
+
throw new Error("Model wind speed must be finite and positive");
|
|
18
|
+
const height = wind.height ?? fallbackHeight;
|
|
19
|
+
if (!Number.isFinite(height) || height <= 0)
|
|
20
|
+
throw new Error("Model wind height must be finite and positive");
|
|
21
|
+
const direction = new THREE.Vector2(wind.direction?.[0] ?? windDirection.x, wind.direction?.[1] ?? windDirection.y);
|
|
22
|
+
if (direction.lengthSq() === 0)
|
|
23
|
+
direction.set(1, 0);
|
|
24
|
+
direction.normalize();
|
|
25
|
+
return { strength, speed, height, direction };
|
|
26
|
+
}
|
|
27
|
+
const VERTEX_PARS = /* glsl */ `
|
|
28
|
+
uniform float uJgWindTime;
|
|
29
|
+
uniform vec2 uJgWindDir;
|
|
30
|
+
uniform float uJgWindStrength;
|
|
31
|
+
uniform float uJgWindSpeed;
|
|
32
|
+
uniform float uJgWindHeight;`;
|
|
33
|
+
// Height-ramped sway in object space, phased by the instance's world position so neighbouring
|
|
34
|
+
// plants move on different gusts. Trunks (low vertices) barely move; fronds ride the full travel.
|
|
35
|
+
const VERTEX_SWAY = /* glsl */ `#include <begin_vertex>
|
|
36
|
+
{
|
|
37
|
+
float jgWindT = clamp(position.y / max(uJgWindHeight, 0.001), 0.0, 1.0);
|
|
38
|
+
float jgWindRamp = jgWindT * jgWindT;
|
|
39
|
+
#ifdef USE_INSTANCING
|
|
40
|
+
vec3 jgWindWorld = (instanceMatrix * vec4(transformed, 1.0)).xyz;
|
|
41
|
+
#else
|
|
42
|
+
vec3 jgWindWorld = (modelMatrix * vec4(transformed, 1.0)).xyz;
|
|
43
|
+
#endif
|
|
44
|
+
float jgWindPhase = dot(jgWindWorld.xz, vec2(0.17, 0.13)) + uJgWindTime * uJgWindSpeed;
|
|
45
|
+
float jgWindGust = sin(jgWindPhase) * 0.6 + sin(jgWindPhase * 0.47 + 2.3) * 0.4;
|
|
46
|
+
float jgWindFlutter = sin(uJgWindTime * 6.3 + jgWindWorld.x * 0.9 + jgWindWorld.z * 1.3) * 0.18;
|
|
47
|
+
vec2 jgWindPush = uJgWindDir * (jgWindGust + jgWindFlutter) * uJgWindStrength * jgWindRamp;
|
|
48
|
+
transformed.x += jgWindPush.x;
|
|
49
|
+
transformed.z += jgWindPush.y;
|
|
50
|
+
}`;
|
|
51
|
+
/**
|
|
52
|
+
* Compiles height-ramped wind sway into a standard material's vertex shader. Idempotent per
|
|
53
|
+
* material; the clock and direction are shared module uniforms, so the whole scene advances with
|
|
54
|
+
* one write per frame and no per-material bookkeeping.
|
|
55
|
+
* @capability model-wind sway a foliage model in the shared gust field
|
|
56
|
+
*/
|
|
57
|
+
export function applyModelWind(material, wind) {
|
|
58
|
+
if (material.userData[WIND_KEY] !== undefined)
|
|
59
|
+
return material;
|
|
60
|
+
const uniforms = {
|
|
61
|
+
uJgWindTime: windClock,
|
|
62
|
+
uJgWindDir: { value: wind.direction.clone() },
|
|
63
|
+
uJgWindStrength: { value: wind.strength },
|
|
64
|
+
uJgWindSpeed: { value: wind.speed },
|
|
65
|
+
uJgWindHeight: { value: wind.height },
|
|
66
|
+
};
|
|
67
|
+
material.userData[WIND_KEY] = uniforms;
|
|
68
|
+
const previousCompile = material.onBeforeCompile;
|
|
69
|
+
const previousKey = material.customProgramCacheKey;
|
|
70
|
+
material.onBeforeCompile = (shader, renderer) => {
|
|
71
|
+
previousCompile.call(material, shader, renderer);
|
|
72
|
+
Object.assign(shader.uniforms, uniforms);
|
|
73
|
+
shader.vertexShader = shader.vertexShader
|
|
74
|
+
.replace("#include <common>", `#include <common>\n${VERTEX_PARS}`)
|
|
75
|
+
.replace("#include <begin_vertex>", VERTEX_SWAY);
|
|
76
|
+
};
|
|
77
|
+
material.customProgramCacheKey = () => `${previousKey.call(material)}|jg-wind:v1`;
|
|
78
|
+
material.needsUpdate = true;
|
|
79
|
+
return material;
|
|
80
|
+
}
|
|
81
|
+
/** Applies wind to every standard material under a cloned model. @internal */
|
|
82
|
+
export function applyModelWindToScene(root, wind, fallbackHeight = 4) {
|
|
83
|
+
const resolved = resolveModelWind(wind, fallbackHeight);
|
|
84
|
+
if (resolved === null)
|
|
85
|
+
return;
|
|
86
|
+
root.traverse((node) => {
|
|
87
|
+
const mesh = node;
|
|
88
|
+
if (!mesh.isMesh)
|
|
89
|
+
return;
|
|
90
|
+
const list = Array.isArray(mesh.material) ? mesh.material : [mesh.material];
|
|
91
|
+
for (const material of list) {
|
|
92
|
+
if (material.isMeshStandardMaterial === true)
|
|
93
|
+
applyModelWind(material, resolved);
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
/** Advances the shared gust clock once per frame; mount one per 3D world. */
|
|
98
|
+
export function WindClock() {
|
|
99
|
+
useFrame(({ clock }) => {
|
|
100
|
+
windClock.value = clock.getElapsedTime();
|
|
101
|
+
});
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { DocumentColorGrade } from "@jgengine/core/editor/environment";
|
|
2
|
+
import { type GameLookInput, type ResolvedGameLook } from "@jgengine/core/render/lookPreset";
|
|
3
|
+
import { type SkyEnvironmentDescriptor } from "@jgengine/core/world/features";
|
|
4
|
+
/** The look the shell renders, resolved once per authored value. @internal */
|
|
5
|
+
export interface PresentationLook {
|
|
6
|
+
look: ResolvedGameLook;
|
|
7
|
+
backdropSky: SkyEnvironmentDescriptor | undefined;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Resolve a game's look preset and backdrop sky, keeping one identity until an authored value
|
|
11
|
+
* changes. The post chain, sky dome and lighting rebuild on identity, so a fresh object per
|
|
12
|
+
* render recompiled every pass each frame.
|
|
13
|
+
* @internal
|
|
14
|
+
*/
|
|
15
|
+
export declare function usePresentationLook(input: GameLookInput, authoredGrade: DocumentColorGrade | undefined): PresentationLook;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { useMemo } from "react";
|
|
2
|
+
import { resolveGameLook } from "@jgengine/core/render/lookPreset";
|
|
3
|
+
import { sky as resolveSkyDescriptor } from "@jgengine/core/world/features";
|
|
4
|
+
import { withAuthoredGrade } from "./authoredGrade.js";
|
|
5
|
+
/**
|
|
6
|
+
* Resolve a game's look preset and backdrop sky, keeping one identity until an authored value
|
|
7
|
+
* changes. The post chain, sky dome and lighting rebuild on identity, so a fresh object per
|
|
8
|
+
* render recompiled every pass each frame.
|
|
9
|
+
* @internal
|
|
10
|
+
*/
|
|
11
|
+
export function usePresentationLook(input, authoredGrade) {
|
|
12
|
+
const { look, lighting, backdrop, postProcessing, hasWorldSky } = input;
|
|
13
|
+
return useMemo(() => {
|
|
14
|
+
const resolved = withAuthoredGrade(resolveGameLook({ look, lighting, backdrop, postProcessing, hasWorldSky }), authoredGrade);
|
|
15
|
+
const sky = resolved.backdrop?.sky;
|
|
16
|
+
return { look: resolved, backdropSky: sky === undefined ? undefined : resolveSkyDescriptor(sky) };
|
|
17
|
+
}, [look, lighting, backdrop, postProcessing, hasWorldSky, authoredGrade]);
|
|
18
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** Whether two texture sets hold the same roles and texture instances. @internal */
|
|
2
|
+
export declare function sameTextureSet(a: unknown, b: unknown): boolean;
|
|
3
|
+
/**
|
|
4
|
+
* Keep one texture-set identity while its roles and texture instances are unchanged. The drei
|
|
5
|
+
* loader hands back a fresh keyed object per render, and materials built from it rebuilt and
|
|
6
|
+
* recompiled their shaders every frame.
|
|
7
|
+
* @internal
|
|
8
|
+
*/
|
|
9
|
+
export declare function useStableTextureSet<T extends object>(textures: T): T;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { useRef } from "react";
|
|
2
|
+
/** Whether two texture sets hold the same roles and texture instances. @internal */
|
|
3
|
+
export function sameTextureSet(a, b) {
|
|
4
|
+
if (a === b)
|
|
5
|
+
return true;
|
|
6
|
+
if (typeof a !== "object" || typeof b !== "object" || a === null || b === null)
|
|
7
|
+
return false;
|
|
8
|
+
const keys = Object.keys(a);
|
|
9
|
+
if (keys.length !== Object.keys(b).length)
|
|
10
|
+
return false;
|
|
11
|
+
return keys.every((key) => a[key] === b[key]);
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Keep one texture-set identity while its roles and texture instances are unchanged. The drei
|
|
15
|
+
* loader hands back a fresh keyed object per render, and materials built from it rebuilt and
|
|
16
|
+
* recompiled their shaders every frame.
|
|
17
|
+
* @internal
|
|
18
|
+
*/
|
|
19
|
+
export function useStableTextureSet(textures) {
|
|
20
|
+
const stable = useRef(textures);
|
|
21
|
+
if (!sameTextureSet(stable.current, textures))
|
|
22
|
+
stable.current = textures;
|
|
23
|
+
return stable.current;
|
|
24
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { MaterialAsset } from "@jgengine/core/material/materialAsset";
|
|
2
|
+
import * as THREE from "three";
|
|
3
|
+
/** Inputs for {@link applyTriplanarProjection}. */
|
|
4
|
+
export interface TriplanarProjectionOptions {
|
|
5
|
+
/** Metres one texture tile covers in world space. Must be finite and positive. */
|
|
6
|
+
tileMetres: number;
|
|
7
|
+
/** Blend sharpness between the three projections; 4 keeps corners soft, 8 is decisive. */
|
|
8
|
+
sharpness?: number;
|
|
9
|
+
/** Normal strength applied to each projection's tangent-space perturbation. */
|
|
10
|
+
normalScale?: number;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* The GLSL the triplanar path injects, split by chunk. Exposed so a test can prove the patch
|
|
14
|
+
* samples by world position instead of `vMapUv`, and so a reader can audit the projection.
|
|
15
|
+
* @internal
|
|
16
|
+
*/
|
|
17
|
+
export declare function triplanarChunks(): {
|
|
18
|
+
vertexPars: string;
|
|
19
|
+
vertexNormal: string;
|
|
20
|
+
vertexPosition: string;
|
|
21
|
+
fragmentPars: string;
|
|
22
|
+
color: string;
|
|
23
|
+
roughness: string;
|
|
24
|
+
normal: string;
|
|
25
|
+
ao: string;
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Projects a standard material's maps from world position instead of mesh UVs, so world geometry
|
|
29
|
+
* without a `uv` attribute still renders a textured PBR surface. Instanced meshes project in their
|
|
30
|
+
* instanced world space. Skinned meshes keep the bind-pose normal for the blend weights, which is
|
|
31
|
+
* close enough for the static world surfaces this path exists for.
|
|
32
|
+
*/
|
|
33
|
+
export declare function applyTriplanarProjection(material: THREE.MeshStandardMaterial, options: TriplanarProjectionOptions): THREE.MeshStandardMaterial;
|
|
34
|
+
/**
|
|
35
|
+
* Render a serializable material asset — one built by `libraryMaterialAsset`, or authored in the
|
|
36
|
+
* editor — with world-space triplanar projection, so world geometry needs no UVs. Tile size
|
|
37
|
+
* defaults to the asset's own `physicalSize`, which the shared library sets per surface.
|
|
38
|
+
* @capability material-triplanar render a material asset on world geometry without UVs
|
|
39
|
+
*/
|
|
40
|
+
export declare function useTriplanarAssetMaterial(asset: MaterialAsset, options?: {
|
|
41
|
+
tileMetres?: number;
|
|
42
|
+
sharpness?: number;
|
|
43
|
+
normalScale?: number;
|
|
44
|
+
}): THREE.MeshStandardMaterial;
|
|
45
|
+
/** Mounts {@link useTriplanarAssetMaterial} on a mesh; the R3F renderer owns disposal. */
|
|
46
|
+
export declare function TriplanarAssetMaterial({ asset, ...options }: {
|
|
47
|
+
asset: MaterialAsset;
|
|
48
|
+
} & {
|
|
49
|
+
tileMetres?: number;
|
|
50
|
+
sharpness?: number;
|
|
51
|
+
normalScale?: number;
|
|
52
|
+
}): import("react").JSX.Element;
|