@jgengine/shell 0.18.0 → 0.19.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 +265 -2
- package/dist/GamePlayerShell.js +27 -4
- package/dist/JoinGate.d.ts +17 -0
- package/dist/JoinGate.js +14 -0
- package/dist/Shell3dPresentation.d.ts +9 -8
- package/dist/Shell3dPresentation.js +57 -8
- package/dist/ShellChrome.d.ts +2 -0
- package/dist/ShellChrome.js +6 -0
- package/dist/ShellHudPresentation.d.ts +9 -1
- package/dist/ShellHudPresentation.js +21 -0
- package/dist/TerritoryOverlay.d.ts +21 -0
- package/dist/TerritoryOverlay.js +9 -0
- package/dist/audio/AudioComponents.js +13 -2
- package/dist/audio/audioEngine.d.ts +15 -1
- package/dist/audio/audioEngine.js +132 -11
- package/dist/audio/audioWire.d.ts +3 -0
- package/dist/audio/audioWire.js +7 -1
- package/dist/audio/loopParams.d.ts +39 -0
- package/dist/audio/loopParams.js +43 -0
- package/dist/camera/GameCameraRig.js +1 -1
- package/dist/camera/GameFirstPersonCamera.d.ts +4 -2
- package/dist/camera/GameFirstPersonCamera.js +52 -15
- package/dist/camera/Viewports.d.ts +34 -0
- package/dist/camera/Viewports.js +123 -0
- package/dist/camera/cameraRigs.js +77 -62
- package/dist/camera/orbitCameraMath.js +2 -2
- package/dist/camera/rigMath.d.ts +82 -4
- package/dist/camera/rigMath.js +133 -15
- package/dist/defineGame.d.ts +9 -0
- package/dist/defineGame.js +38 -6
- package/dist/devtools/DevtoolsOverlay.js +2 -0
- package/dist/devtools/KeysPanel.js +3 -3
- package/dist/devtools/agentBridge.d.ts +6 -0
- package/dist/devtools/agentBridge.js +15 -1
- package/dist/diagnostics/FpsCounter.d.ts +17 -0
- package/dist/diagnostics/FpsCounter.js +32 -0
- package/dist/drivers/FrameDriver.d.ts +5 -1
- package/dist/drivers/FrameDriver.js +74 -26
- package/dist/drivers/FrameRateLimiter.d.ts +9 -0
- package/dist/drivers/FrameRateLimiter.js +39 -0
- package/dist/drivers/HudOnlyDriver.js +10 -5
- package/dist/environment/DayNightSky.js +5 -5
- package/dist/environment/Daylight.d.ts +35 -1
- package/dist/environment/Daylight.js +98 -28
- package/dist/environment/EnvironmentScene.js +1 -1
- package/dist/environment/VolumetricClouds.js +20 -2
- package/dist/environment/daylightCycle.d.ts +20 -0
- package/dist/environment/daylightCycle.js +59 -7
- package/dist/environment/sunShadowMath.d.ts +45 -0
- package/dist/environment/sunShadowMath.js +50 -0
- package/dist/input/gamepadMerge.d.ts +10 -0
- package/dist/input/gamepadMerge.js +12 -0
- package/dist/input/gamepadPoll.d.ts +47 -0
- package/dist/input/gamepadPoll.js +142 -0
- package/dist/input/gamepadSource.d.ts +22 -0
- package/dist/input/gamepadSource.js +153 -0
- package/dist/input/mouseLook.js +2 -1
- package/dist/input/padHaptics.d.ts +17 -0
- package/dist/input/padHaptics.js +32 -0
- package/dist/input/pointerLock.d.ts +11 -0
- package/dist/input/pointerLock.js +19 -0
- package/dist/materialOverride.d.ts +4 -1
- package/dist/materialOverride.js +10 -1
- package/dist/postfx/PostProcessing.d.ts +11 -8
- package/dist/postfx/PostProcessing.js +38 -16
- package/dist/postfx/gradeShader.js +13 -0
- package/dist/postfx/stylizePass.d.ts +23 -0
- package/dist/postfx/stylizePass.js +216 -0
- package/dist/render/CascadedShadows.d.ts +11 -0
- package/dist/render/CascadedShadows.js +19 -8
- package/dist/render/EnvironmentLighting.d.ts +4 -1
- package/dist/render/EnvironmentLighting.js +45 -9
- package/dist/render/SceneLighting.d.ts +16 -2
- package/dist/render/SceneLighting.js +56 -6
- package/dist/render/SceneModels.js +45 -9
- package/dist/render/SkinnedInstances.d.ts +91 -0
- package/dist/render/SkinnedInstances.js +249 -0
- package/dist/render/TextureFiltering.d.ts +16 -0
- package/dist/render/TextureFiltering.js +64 -0
- package/dist/render/displayColor.d.ts +33 -0
- package/dist/render/displayColor.js +138 -0
- package/dist/render/modelLoad.d.ts +39 -0
- package/dist/render/modelLoad.js +89 -1
- package/dist/render/modelRender.d.ts +3 -0
- package/dist/render/modelRender.js +6 -2
- package/dist/render/resolveModel.js +4 -0
- package/dist/render/useFootIk.d.ts +58 -0
- package/dist/render/useFootIk.js +216 -0
- package/dist/render/useModelAnimation.d.ts +26 -0
- package/dist/render/useModelAnimation.js +184 -107
- package/dist/scene/AuthoredScene.d.ts +9 -0
- package/dist/scene/AuthoredScene.js +17 -1
- package/dist/scene/CityRenderer.js +666 -241
- package/dist/scene/cityWindowMaterial.d.ts +8 -6
- package/dist/scene/cityWindowMaterial.js +80 -21
- package/dist/settings/QuickControls.js +1 -1
- package/dist/settings/SettingsMenu.js +1 -1
- package/dist/settings/appliedSettings.d.ts +10 -2
- package/dist/settings/appliedSettings.js +9 -5
- package/dist/settings/settingsController.d.ts +3 -0
- package/dist/settings/settingsController.js +47 -3
- package/dist/structures/BuildingKitBatch.js +26 -22
- package/dist/structures/GeneratedBuilding.d.ts +3 -15
- package/dist/structures/GeneratedBuilding.js +21 -15
- package/dist/structures/buildingSurface.d.ts +21 -0
- package/dist/structures/buildingSurface.js +134 -0
- package/dist/terrain/terrainDetailMaterial.js +7 -3
- package/dist/useShellMultiplayerSync.d.ts +9 -2
- package/dist/useShellMultiplayerSync.js +30 -8
- package/dist/visibility/CullingProvider.d.ts +2 -1
- package/dist/visibility/CullingProvider.js +7 -2
- package/dist/world/GridWorldScene.js +4 -0
- package/dist/world/TileLayerRenderer.d.ts +9 -0
- package/dist/world/TileLayerRenderer.js +64 -0
- package/dist/world/WorldParticles.d.ts +31 -0
- package/dist/world/WorldParticles.js +128 -0
- package/dist/world/WorldScene.d.ts +2 -1
- package/dist/world/WorldScene.js +46 -4
- package/dist/world/entityPose.d.ts +4 -0
- package/dist/world/entityPose.js +9 -0
- package/dist/world/tileLayerInstances.d.ts +11 -0
- package/dist/world/tileLayerInstances.js +28 -0
- package/dist/worldSync.d.ts +5 -1
- package/dist/worldSync.js +25 -4
- package/package.json +4 -4
- package/skills/ce-handoff/SKILL.md +2 -2
- package/skills/game-design/SKILL.md +11 -1
- package/skills/jgengine/SKILL.md +9 -5
- package/skills/jgengine/api.md +205 -101
- package/skills/jgengine/capabilities.md +56 -0
- package/skills/jgengine/recipes/minimal-game.md +3 -0
- package/skills/jgengine-ui/SKILL.md +20 -15
- package/skills/jgengine-ui/api.md +538 -326
- package/skills/jgengine-ui/capabilities.md +37 -0
- package/skills/jgengine-ui/reference-primitives.md +89 -0
- package/skills/jgengine-ui/reference.md +2 -78
- package/skills/jgengine-world/SKILL.md +21 -3
- package/skills/jgengine-world/api.md +1172 -625
- package/skills/jgengine-world/capabilities.md +229 -3
- package/skills/jgengine-world/recipes/character-animation.md +39 -0
- package/skills/jgengine-world/recipes/controls.md +44 -0
- package/skills/jgengine-world/recipes/flight-feel.md +76 -0
- package/skills/jgengine-world/recipes/npc-senses.md +43 -0
- package/skills/jgengine-world/recipes/vehicle-feel.md +85 -0
- package/skills/jgengine-world/reference.md +31 -2
- package/skills/jgengine-world/visibility.md +112 -0
- package/skills/level-design/SKILL.md +1 -1
- package/skills/jgengine-ui/references/visual-scorecard.md +0 -73
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
interface LockableElement {
|
|
2
|
+
requestPointerLock?: (options?: {
|
|
3
|
+
unadjustedMovement?: boolean;
|
|
4
|
+
}) => Promise<void> | void;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Request pointer lock with raw (unaccelerated) mouse deltas, falling back to a plain lock when
|
|
8
|
+
* the browser or OS rejects `unadjustedMovement`.
|
|
9
|
+
*/
|
|
10
|
+
export declare function requestRawPointerLock(element: LockableElement): void;
|
|
11
|
+
export {};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Request pointer lock with raw (unaccelerated) mouse deltas, falling back to a plain lock when
|
|
3
|
+
* the browser or OS rejects `unadjustedMovement`.
|
|
4
|
+
*/
|
|
5
|
+
export function requestRawPointerLock(element) {
|
|
6
|
+
const request = element.requestPointerLock;
|
|
7
|
+
if (request === undefined)
|
|
8
|
+
return;
|
|
9
|
+
let pending;
|
|
10
|
+
try {
|
|
11
|
+
pending = request.call(element, { unadjustedMovement: true });
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
pending = request.call(element);
|
|
15
|
+
}
|
|
16
|
+
if (pending !== undefined && typeof pending.catch === "function") {
|
|
17
|
+
pending.catch(() => request.call(element)?.catch?.(() => undefined));
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -2,7 +2,7 @@ import * as THREE from "three";
|
|
|
2
2
|
import type { ModelMaterialOverride } from "@jgengine/core/game/playableGame";
|
|
3
3
|
export interface MaterialOverrideOptions {
|
|
4
4
|
clone?: boolean;
|
|
5
|
-
/** Loaded PBR textures matching `ModelMaterialOverride.maps`' roles, applied onto each
|
|
5
|
+
/** Loaded PBR textures matching `ModelMaterialOverride.maps`' roles, applied onto each standard or physical material alongside the tint override. The caller (a React component, via `useTexture`) owns loading; this function never fetches a URL. */
|
|
6
6
|
textures?: MaterialOverrideTextures;
|
|
7
7
|
}
|
|
8
8
|
/** Loaded PBR textures for `applyMaterialOverride`'s `textures` option — matches `ModelMaterialMaps`' roles. */
|
|
@@ -11,6 +11,9 @@ export interface MaterialOverrideTextures {
|
|
|
11
11
|
normal?: THREE.Texture;
|
|
12
12
|
roughness?: THREE.Texture;
|
|
13
13
|
ao?: THREE.Texture;
|
|
14
|
+
metalness?: THREE.Texture;
|
|
15
|
+
emissive?: THREE.Texture;
|
|
16
|
+
height?: THREE.Texture;
|
|
14
17
|
}
|
|
15
18
|
/** @internal */
|
|
16
19
|
export declare function applyMaterialOverride(root: THREE.Object3D, override: ModelMaterialOverride, options?: MaterialOverrideOptions): void;
|
package/dist/materialOverride.js
CHANGED
|
@@ -13,7 +13,7 @@ export function applyMaterialOverride(root, override, options) {
|
|
|
13
13
|
});
|
|
14
14
|
}
|
|
15
15
|
function overrideOne(material, override, clone, textures) {
|
|
16
|
-
if (!(material
|
|
16
|
+
if (!isStandardOrPhysicalMaterial(material))
|
|
17
17
|
return material;
|
|
18
18
|
const target = clone ? material.clone() : material;
|
|
19
19
|
if (override.color !== undefined)
|
|
@@ -34,12 +34,21 @@ function overrideOne(material, override, clone, textures) {
|
|
|
34
34
|
target.roughnessMap = textures.roughness;
|
|
35
35
|
if (textures?.ao !== undefined)
|
|
36
36
|
target.aoMap = textures.ao;
|
|
37
|
+
if (textures?.metalness !== undefined)
|
|
38
|
+
target.metalnessMap = textures.metalness;
|
|
39
|
+
if (textures?.emissive !== undefined)
|
|
40
|
+
target.emissiveMap = textures.emissive;
|
|
41
|
+
if (textures?.height !== undefined)
|
|
42
|
+
target.displacementMap = textures.height;
|
|
37
43
|
if (textures !== undefined)
|
|
38
44
|
target.needsUpdate = true;
|
|
39
45
|
if (override.rim !== undefined)
|
|
40
46
|
applyRimLight(target, override.rim);
|
|
41
47
|
return target;
|
|
42
48
|
}
|
|
49
|
+
function isStandardOrPhysicalMaterial(material) {
|
|
50
|
+
return material.isMeshStandardMaterial === true;
|
|
51
|
+
}
|
|
43
52
|
/**
|
|
44
53
|
* Adds a fresnel rim term to a standard material's outgoing light. Patched through
|
|
45
54
|
* `onBeforeCompile` rather than as a second pass so the rim shadows, fogs, and tonemaps with the
|
|
@@ -3,17 +3,20 @@ import type { GraphicsQuality } from "@jgengine/core/settings/settingsModel";
|
|
|
3
3
|
/**
|
|
4
4
|
* Mounts an `EffectComposer` inside the shell Canvas and takes over rendering
|
|
5
5
|
* (priority-1 `useFrame`, which disables R3F auto-render) to run the configured
|
|
6
|
-
* post chain: RenderPass → GTAO → UnrealBloom → SMAA → OutputPass → Grade. Rendered only
|
|
6
|
+
* post chain: RenderPass → GTAO → UnrealBloom → SMAA → OutputPass → Stylize → Grade. Rendered only
|
|
7
7
|
* when `PlayableGame.postProcessing` is set, so games without it draw unchanged.
|
|
8
8
|
*
|
|
9
|
-
* `
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* and Bokeh DOF renders scene depth again — both run on "high" only. Bloom,
|
|
13
|
-
* SMAA, tone mapping, and grade stay on every tier (SMAA is the cheap edge fix
|
|
14
|
-
* for alpha-tested foliage that MSAA samples alone leave crawling).
|
|
9
|
+
* `stages` (resolved from the player's graphics profile) controls expensive passes
|
|
10
|
+
* independently. Medium keeps AO and DOF with reduced AO sampling; low can disable
|
|
11
|
+
* them while retaining cheaper bloom, SMAA, tone mapping, and grade stages.
|
|
15
12
|
*/
|
|
16
|
-
export declare function PostProcessing({ config, quality }: {
|
|
13
|
+
export declare function PostProcessing({ config, quality, stages }: {
|
|
17
14
|
config: PostProcessingConfig;
|
|
18
15
|
quality?: GraphicsQuality;
|
|
16
|
+
stages?: {
|
|
17
|
+
ao: boolean;
|
|
18
|
+
bloom: boolean;
|
|
19
|
+
dof: boolean;
|
|
20
|
+
smaa: boolean;
|
|
21
|
+
};
|
|
19
22
|
}): null;
|
|
@@ -7,6 +7,7 @@ import { GTAOPass } from "three/examples/jsm/postprocessing/GTAOPass.js";
|
|
|
7
7
|
import { BokehPass } from "three/examples/jsm/postprocessing/BokehPass.js";
|
|
8
8
|
import { SMAAPass } from "three/examples/jsm/postprocessing/SMAAPass.js";
|
|
9
9
|
import { createGradePass } from "./gradeShader.js";
|
|
10
|
+
import { StylizePass } from "./stylizePass.js";
|
|
10
11
|
import { hidePostfxOverlays, restorePostfxOverlays } from "./postfxOverlay.js";
|
|
11
12
|
/**
|
|
12
13
|
* GTAO renders the scene with an `overrideMaterial` normal/depth prepass, which stamps
|
|
@@ -60,17 +61,14 @@ function syncSize(built, width, height, pixelRatio) {
|
|
|
60
61
|
/**
|
|
61
62
|
* Mounts an `EffectComposer` inside the shell Canvas and takes over rendering
|
|
62
63
|
* (priority-1 `useFrame`, which disables R3F auto-render) to run the configured
|
|
63
|
-
* post chain: RenderPass → GTAO → UnrealBloom → SMAA → OutputPass → Grade. Rendered only
|
|
64
|
+
* post chain: RenderPass → GTAO → UnrealBloom → SMAA → OutputPass → Stylize → Grade. Rendered only
|
|
64
65
|
* when `PlayableGame.postProcessing` is set, so games without it draw unchanged.
|
|
65
66
|
*
|
|
66
|
-
* `
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
* and Bokeh DOF renders scene depth again — both run on "high" only. Bloom,
|
|
70
|
-
* SMAA, tone mapping, and grade stay on every tier (SMAA is the cheap edge fix
|
|
71
|
-
* for alpha-tested foliage that MSAA samples alone leave crawling).
|
|
67
|
+
* `stages` (resolved from the player's graphics profile) controls expensive passes
|
|
68
|
+
* independently. Medium keeps AO and DOF with reduced AO sampling; low can disable
|
|
69
|
+
* them while retaining cheaper bloom, SMAA, tone mapping, and grade stages.
|
|
72
70
|
*/
|
|
73
|
-
export function PostProcessing({ config, quality = "high" }) {
|
|
71
|
+
export function PostProcessing({ config, quality = "high", stages }) {
|
|
74
72
|
const gl = useThree((s) => s.gl);
|
|
75
73
|
const scene = useThree((s) => s.scene);
|
|
76
74
|
const camera = useThree((s) => s.camera);
|
|
@@ -87,8 +85,8 @@ export function PostProcessing({ config, quality = "high" }) {
|
|
|
87
85
|
});
|
|
88
86
|
const composer = new EffectComposer(gl, target);
|
|
89
87
|
composer.addPass(new RenderPass(scene, camera));
|
|
90
|
-
const
|
|
91
|
-
if (
|
|
88
|
+
const resolvedStages = stages ?? { ao: quality === "high", bloom: true, dof: quality === "high", smaa: true };
|
|
89
|
+
if (resolvedStages.ao && config.ao !== undefined && config.ao !== false) {
|
|
92
90
|
const ao = new OverlayAwareGTAOPass(scene, camera, width, height);
|
|
93
91
|
ao.blendIntensity = config.ao.blend ?? 1;
|
|
94
92
|
ao.updateGtaoMaterial({
|
|
@@ -96,29 +94,32 @@ export function PostProcessing({ config, quality = "high" }) {
|
|
|
96
94
|
distanceExponent: 1,
|
|
97
95
|
thickness: config.ao.distanceFalloff ?? 3.6,
|
|
98
96
|
scale: config.ao.intensity ?? 2.4,
|
|
99
|
-
samples: 16,
|
|
97
|
+
samples: quality === "high" ? 16 : 8,
|
|
100
98
|
});
|
|
101
99
|
composer.addPass(ao);
|
|
102
100
|
}
|
|
103
|
-
if (config.bloom !== false) {
|
|
101
|
+
if (resolvedStages.bloom && config.bloom !== false) {
|
|
104
102
|
const b = config.bloom ?? {};
|
|
105
103
|
composer.addPass(new UnrealBloomPass(new THREE.Vector2(width, height), b.strength ?? 0.32, b.radius ?? 0.55, b.threshold ?? 0.85));
|
|
106
104
|
}
|
|
107
105
|
let dof = null;
|
|
108
|
-
if (
|
|
106
|
+
if (resolvedStages.dof && config.dof !== undefined && config.dof !== false) {
|
|
109
107
|
const d = config.dof;
|
|
110
108
|
dof = new OverlayAwareBokehPass(scene, camera, {
|
|
111
109
|
focus: d.focus ?? 18,
|
|
112
|
-
aperture: d.aperture ?? 0.00025,
|
|
110
|
+
aperture: d.aperture ?? (quality === "high" ? 0.00025 : 0.00012),
|
|
113
111
|
maxblur: d.maxBlur ?? 0.01,
|
|
114
112
|
});
|
|
115
113
|
composer.addPass(dof);
|
|
116
114
|
}
|
|
117
115
|
// SMAA must run before OutputPass (linear-sRGB). Default on for post chains.
|
|
118
|
-
if (aa === "smaa") {
|
|
116
|
+
if (resolvedStages.smaa && aa === "smaa") {
|
|
119
117
|
composer.addPass(new SMAAPass());
|
|
120
118
|
}
|
|
121
119
|
composer.addPass(new OutputPass());
|
|
120
|
+
if (config.stylize !== undefined && config.stylize !== false) {
|
|
121
|
+
composer.addPass(new StylizePass(scene, camera, config.stylize));
|
|
122
|
+
}
|
|
122
123
|
let grade = null;
|
|
123
124
|
if (config.grade !== false) {
|
|
124
125
|
grade = createGradePass(config.grade ?? {});
|
|
@@ -131,7 +132,28 @@ export function PostProcessing({ config, quality = "high" }) {
|
|
|
131
132
|
disposeGraph(graph);
|
|
132
133
|
builtRef.current = null;
|
|
133
134
|
};
|
|
134
|
-
}, [gl, scene, camera, config, quality]);
|
|
135
|
+
}, [gl, scene, camera, config, quality, stages]);
|
|
136
|
+
useEffect(() => {
|
|
137
|
+
const lut = config.grade !== undefined && config.grade !== false ? config.grade.lut : undefined;
|
|
138
|
+
const grade = builtRef.current?.grade;
|
|
139
|
+
if (lut === undefined || grade === null || grade === undefined)
|
|
140
|
+
return;
|
|
141
|
+
let cancelled = false;
|
|
142
|
+
const loader = new THREE.TextureLoader();
|
|
143
|
+
loader.loadAsync(lut.url).then((texture) => {
|
|
144
|
+
if (cancelled) {
|
|
145
|
+
texture.dispose();
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
texture.colorSpace = THREE.SRGBColorSpace;
|
|
149
|
+
texture.minFilter = THREE.NearestFilter;
|
|
150
|
+
texture.magFilter = THREE.NearestFilter;
|
|
151
|
+
texture.generateMipmaps = false;
|
|
152
|
+
grade.uniforms.uLut.value = texture;
|
|
153
|
+
grade.uniforms.uLutSize.value = lut.size ?? 32;
|
|
154
|
+
}).catch(() => undefined);
|
|
155
|
+
return () => { cancelled = true; };
|
|
156
|
+
}, [config.grade]);
|
|
135
157
|
useEffect(() => {
|
|
136
158
|
const prevTone = gl.toneMapping;
|
|
137
159
|
const prevExposure = gl.toneMappingExposure;
|
|
@@ -17,12 +17,22 @@ const fragmentShader = /* glsl */ `
|
|
|
17
17
|
uniform float uGrain;
|
|
18
18
|
uniform float uChroma;
|
|
19
19
|
uniform float uTime;
|
|
20
|
+
uniform sampler2D uLut;
|
|
21
|
+
uniform float uLutSize;
|
|
20
22
|
varying vec2 vUv;
|
|
21
23
|
|
|
22
24
|
float hash(vec2 p) {
|
|
23
25
|
return fract(sin(dot(p, vec2(12.9898, 78.233))) * 43758.5453);
|
|
24
26
|
}
|
|
25
27
|
|
|
28
|
+
vec3 sampleLut(vec3 color) {
|
|
29
|
+
float size = max(uLutSize, 2.0);
|
|
30
|
+
vec3 c = clamp(color, 0.0, 1.0) * (size - 1.0);
|
|
31
|
+
float blue = floor(c.b);
|
|
32
|
+
vec2 uv = vec2((blue * size + c.r + 0.5) / (size * size), (c.g + 0.5) / size);
|
|
33
|
+
return texture2D(uLut, uv).rgb;
|
|
34
|
+
}
|
|
35
|
+
|
|
26
36
|
void main() {
|
|
27
37
|
vec2 dir = vUv - 0.5;
|
|
28
38
|
// Chromatic aberration: split RGB along the radial direction, growing toward the edges.
|
|
@@ -35,6 +45,7 @@ const fragmentShader = /* glsl */ `
|
|
|
35
45
|
c = pow(max(c, 0.0), vec3(uGamma));
|
|
36
46
|
float luma = dot(c, vec3(0.2126, 0.7152, 0.0722));
|
|
37
47
|
c = mix(vec3(luma), c, uSaturation);
|
|
48
|
+
if (uLutSize > 1.0) c = sampleLut(c);
|
|
38
49
|
vec2 d = vUv - 0.5;
|
|
39
50
|
float vig = 1.0 - uVignette * smoothstep(0.6, 0.95, dot(d, d) * 2.2);
|
|
40
51
|
c *= vig;
|
|
@@ -65,6 +76,8 @@ export function createGradePass(config = {}) {
|
|
|
65
76
|
uGrain: { value: config.grain ?? DEFAULT_GRAIN },
|
|
66
77
|
uChroma: { value: config.chromaticAberration ?? DEFAULT_CHROMA },
|
|
67
78
|
uTime: { value: 0 },
|
|
79
|
+
uLut: { value: null },
|
|
80
|
+
uLutSize: { value: 0 },
|
|
68
81
|
},
|
|
69
82
|
vertexShader,
|
|
70
83
|
fragmentShader,
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import * as THREE from "three";
|
|
2
|
+
import { Pass } from "three-stdlib";
|
|
3
|
+
import type { StylizeConfig } from "@jgengine/core/render/postProcessing";
|
|
4
|
+
/**
|
|
5
|
+
* Art-style post pass: ink outlines from a normal+depth prepass, cel luminance bands, and
|
|
6
|
+
* pixelation. Runs on the display-space frame after `OutputPass`. The prepass renders only
|
|
7
|
+
* when outlines are on, and reuses the frame's shadow maps instead of re-rendering them.
|
|
8
|
+
*/
|
|
9
|
+
export declare class StylizePass extends Pass {
|
|
10
|
+
private readonly scene;
|
|
11
|
+
private readonly camera;
|
|
12
|
+
private readonly quad;
|
|
13
|
+
private readonly material;
|
|
14
|
+
private readonly normalTarget;
|
|
15
|
+
private readonly normalMaterial;
|
|
16
|
+
private readonly hidden;
|
|
17
|
+
private readonly clearColor;
|
|
18
|
+
constructor(scene: THREE.Scene, camera: THREE.Camera, config: StylizeConfig);
|
|
19
|
+
setSize(width: number, height: number): void;
|
|
20
|
+
render(renderer: THREE.WebGLRenderer, writeBuffer: THREE.WebGLRenderTarget, readBuffer: THREE.WebGLRenderTarget): void;
|
|
21
|
+
private renderPrepass;
|
|
22
|
+
dispose(): void;
|
|
23
|
+
}
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import * as THREE from "three";
|
|
2
|
+
import { FullScreenQuad, Pass } from "three-stdlib";
|
|
3
|
+
import { isPostfxOverlay } from "./postfxOverlay.js";
|
|
4
|
+
const DEFAULT_OUTLINE_COLOR = "#101014";
|
|
5
|
+
const DEFAULT_DEPTH_THRESHOLD = 0.08;
|
|
6
|
+
const DEFAULT_NORMAL_THRESHOLD = 0.35;
|
|
7
|
+
const DEFAULT_FADE_DISTANCE = 160;
|
|
8
|
+
const fragmentShader = /* glsl */ `
|
|
9
|
+
#include <packing>
|
|
10
|
+
uniform sampler2D tDiffuse;
|
|
11
|
+
uniform sampler2D tNormal;
|
|
12
|
+
uniform sampler2D tDepth;
|
|
13
|
+
uniform vec2 uResolution;
|
|
14
|
+
uniform float uNear;
|
|
15
|
+
uniform float uFar;
|
|
16
|
+
uniform bool uOrtho;
|
|
17
|
+
uniform bool uOutline;
|
|
18
|
+
uniform vec3 uOutlineColor;
|
|
19
|
+
uniform float uOutlineOpacity;
|
|
20
|
+
uniform float uThickness;
|
|
21
|
+
uniform float uDepthThreshold;
|
|
22
|
+
uniform float uNormalThreshold;
|
|
23
|
+
uniform float uFade;
|
|
24
|
+
uniform float uBands;
|
|
25
|
+
uniform float uPixel;
|
|
26
|
+
varying vec2 vUv;
|
|
27
|
+
|
|
28
|
+
float viewDepth(vec2 uv) {
|
|
29
|
+
float d = texture2D(tDepth, uv).x;
|
|
30
|
+
return -(uOrtho ? orthographicDepthToViewZ(d, uNear, uFar) : perspectiveDepthToViewZ(d, uNear, uFar));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
vec3 viewNormal(vec2 uv) {
|
|
34
|
+
return texture2D(tNormal, uv).xyz * 2.0 - 1.0;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Ink a normal crease only on its nearer side, so a thin or tiny feature stays one line wide
|
|
38
|
+
// instead of stamping a plus-shaped blot onto the surface behind it.
|
|
39
|
+
float crease(vec3 nc, float dc, vec2 uv, float dn) {
|
|
40
|
+
return (1.0 - dot(nc, viewNormal(uv))) * step(dc, dn + dc * 0.02);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
void main() {
|
|
44
|
+
vec2 uv = vUv;
|
|
45
|
+
if (uPixel > 1.0) {
|
|
46
|
+
vec2 block = uPixel / uResolution;
|
|
47
|
+
uv = (floor(uv / block) + 0.5) * block;
|
|
48
|
+
}
|
|
49
|
+
vec3 c = texture2D(tDiffuse, uv).rgb;
|
|
50
|
+
|
|
51
|
+
if (uBands > 0.5) {
|
|
52
|
+
float luma = dot(c, vec3(0.2126, 0.7152, 0.0722));
|
|
53
|
+
float x = min(luma * uBands, uBands - 0.001);
|
|
54
|
+
// Each band takes its centre brightness: the darkest band keeps colour instead of going black
|
|
55
|
+
// and the top band never clips a bright sky to flat white. A narrow soft step hides aliasing.
|
|
56
|
+
float q = (floor(x) + 0.5 + smoothstep(0.92, 1.0, fract(x))) / uBands;
|
|
57
|
+
c *= q / max(luma, 1e-4);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (uOutline) {
|
|
61
|
+
vec2 px = uThickness / uResolution;
|
|
62
|
+
float dc = viewDepth(uv);
|
|
63
|
+
float dl = viewDepth(uv - vec2(px.x, 0.0));
|
|
64
|
+
float dr = viewDepth(uv + vec2(px.x, 0.0));
|
|
65
|
+
float dd = viewDepth(uv - vec2(0.0, px.y));
|
|
66
|
+
float du = viewDepth(uv + vec2(0.0, px.y));
|
|
67
|
+
// Laplacian is zero across flat planes at any angle, so only real depth breaks draw lines.
|
|
68
|
+
float lap = abs(dl + dr + dd + du - 4.0 * dc) / max(dc, 1e-3);
|
|
69
|
+
float nearest = min(dc, min(min(dl, dr), min(dd, du)));
|
|
70
|
+
// Same nearer-side rule as creases: the background around a silhouette stays clean.
|
|
71
|
+
float depthEdge = smoothstep(uDepthThreshold, uDepthThreshold * 2.0, lap) * step(dc, nearest + dc * 0.03);
|
|
72
|
+
|
|
73
|
+
vec3 nc = viewNormal(uv);
|
|
74
|
+
float nd = max(
|
|
75
|
+
max(crease(nc, dc, uv - vec2(px.x, 0.0), dl), crease(nc, dc, uv + vec2(px.x, 0.0), dr)),
|
|
76
|
+
max(crease(nc, dc, uv - vec2(0.0, px.y), dd), crease(nc, dc, uv + vec2(0.0, px.y), du))
|
|
77
|
+
);
|
|
78
|
+
float normalEdge = smoothstep(uNormalThreshold, uNormalThreshold + 0.25, nd);
|
|
79
|
+
|
|
80
|
+
float fade = 1.0 - smoothstep(uFade * 0.5, uFade, nearest);
|
|
81
|
+
float edge = max(depthEdge, normalEdge) * fade * uOutlineOpacity;
|
|
82
|
+
c = mix(c, uOutlineColor, clamp(edge, 0.0, 1.0));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
gl_FragColor = vec4(c, 1.0);
|
|
86
|
+
}
|
|
87
|
+
`;
|
|
88
|
+
const vertexShader = /* glsl */ `
|
|
89
|
+
varying vec2 vUv;
|
|
90
|
+
void main() {
|
|
91
|
+
vUv = uv;
|
|
92
|
+
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
|
|
93
|
+
}
|
|
94
|
+
`;
|
|
95
|
+
function skipsDepthPrepass(material) {
|
|
96
|
+
const list = Array.isArray(material) ? material : [material];
|
|
97
|
+
// Shader materials place vertices in their own code, which the override material cannot reproduce.
|
|
98
|
+
return list.some((m) => m.transparent || m.alphaTest > 0 || !m.depthWrite || !m.colorWrite || m.isShaderMaterial === true);
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Hide what the outline prepass must not see: marked overlays, points/lines/sprites, custom
|
|
102
|
+
* shader meshes, and transparent or alpha-cut meshes (foliage cards would otherwise outline as
|
|
103
|
+
* solid quads).
|
|
104
|
+
*/
|
|
105
|
+
function hideForPrepass(scene, out) {
|
|
106
|
+
out.length = 0;
|
|
107
|
+
scene.traverse((object) => {
|
|
108
|
+
if (!object.visible)
|
|
109
|
+
return;
|
|
110
|
+
const drawable = object;
|
|
111
|
+
const hide = isPostfxOverlay(object) ||
|
|
112
|
+
object.isPoints === true ||
|
|
113
|
+
object.isLine === true ||
|
|
114
|
+
object.isSprite === true ||
|
|
115
|
+
(drawable.isMesh === true && skipsDepthPrepass(drawable.material));
|
|
116
|
+
if (!hide)
|
|
117
|
+
return;
|
|
118
|
+
object.visible = false;
|
|
119
|
+
out.push(object);
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Art-style post pass: ink outlines from a normal+depth prepass, cel luminance bands, and
|
|
124
|
+
* pixelation. Runs on the display-space frame after `OutputPass`. The prepass renders only
|
|
125
|
+
* when outlines are on, and reuses the frame's shadow maps instead of re-rendering them.
|
|
126
|
+
*/
|
|
127
|
+
export class StylizePass extends Pass {
|
|
128
|
+
scene;
|
|
129
|
+
camera;
|
|
130
|
+
quad;
|
|
131
|
+
material;
|
|
132
|
+
normalTarget;
|
|
133
|
+
normalMaterial = new THREE.MeshNormalMaterial();
|
|
134
|
+
hidden = [];
|
|
135
|
+
clearColor = new THREE.Color();
|
|
136
|
+
constructor(scene, camera, config) {
|
|
137
|
+
super();
|
|
138
|
+
this.scene = scene;
|
|
139
|
+
this.camera = camera;
|
|
140
|
+
const outline = config.outline === undefined || config.outline === false ? null : config.outline;
|
|
141
|
+
this.normalTarget = outline === null
|
|
142
|
+
? null
|
|
143
|
+
: new THREE.WebGLRenderTarget(1, 1, { depthTexture: new THREE.DepthTexture(1, 1), type: THREE.UnsignedByteType });
|
|
144
|
+
this.material = new THREE.ShaderMaterial({
|
|
145
|
+
uniforms: {
|
|
146
|
+
tDiffuse: { value: null },
|
|
147
|
+
tNormal: { value: this.normalTarget?.texture ?? null },
|
|
148
|
+
tDepth: { value: this.normalTarget?.depthTexture ?? null },
|
|
149
|
+
uResolution: { value: new THREE.Vector2(1, 1) },
|
|
150
|
+
uNear: { value: 0.1 },
|
|
151
|
+
uFar: { value: 1000 },
|
|
152
|
+
uOrtho: { value: false },
|
|
153
|
+
uOutline: { value: outline !== null },
|
|
154
|
+
uOutlineColor: { value: new THREE.Color(outline?.color ?? DEFAULT_OUTLINE_COLOR) },
|
|
155
|
+
uOutlineOpacity: { value: outline?.opacity ?? 1 },
|
|
156
|
+
uThickness: { value: Math.max(0.5, outline?.thickness ?? 1) },
|
|
157
|
+
uDepthThreshold: { value: outline?.depthThreshold ?? DEFAULT_DEPTH_THRESHOLD },
|
|
158
|
+
uNormalThreshold: { value: outline?.normalThreshold ?? DEFAULT_NORMAL_THRESHOLD },
|
|
159
|
+
uFade: { value: outline?.fadeDistance ?? DEFAULT_FADE_DISTANCE },
|
|
160
|
+
uBands: { value: Math.max(0, Math.floor(config.bands ?? 0)) },
|
|
161
|
+
uPixel: { value: Math.max(0, config.pixelSize ?? 0) },
|
|
162
|
+
},
|
|
163
|
+
vertexShader,
|
|
164
|
+
fragmentShader,
|
|
165
|
+
depthTest: false,
|
|
166
|
+
depthWrite: false,
|
|
167
|
+
});
|
|
168
|
+
this.quad = new FullScreenQuad(this.material);
|
|
169
|
+
}
|
|
170
|
+
setSize(width, height) {
|
|
171
|
+
this.material.uniforms.uResolution.value.set(width, height);
|
|
172
|
+
this.normalTarget?.setSize(width, height);
|
|
173
|
+
}
|
|
174
|
+
render(renderer, writeBuffer, readBuffer) {
|
|
175
|
+
const uniforms = this.material.uniforms;
|
|
176
|
+
if (this.normalTarget !== null)
|
|
177
|
+
this.renderPrepass(renderer, this.normalTarget);
|
|
178
|
+
const camera = this.camera;
|
|
179
|
+
uniforms.uNear.value = camera.near;
|
|
180
|
+
uniforms.uFar.value = camera.far;
|
|
181
|
+
uniforms.uOrtho.value = camera.isOrthographicCamera === true;
|
|
182
|
+
uniforms.tDiffuse.value = readBuffer.texture;
|
|
183
|
+
renderer.setRenderTarget(this.renderToScreen ? null : writeBuffer);
|
|
184
|
+
this.quad.render(renderer);
|
|
185
|
+
}
|
|
186
|
+
renderPrepass(renderer, target) {
|
|
187
|
+
const scene = this.scene;
|
|
188
|
+
const previousOverride = scene.overrideMaterial;
|
|
189
|
+
const previousBackground = scene.background;
|
|
190
|
+
const previousShadowUpdate = renderer.shadowMap.autoUpdate;
|
|
191
|
+
const previousClearAlpha = renderer.getClearAlpha();
|
|
192
|
+
renderer.getClearColor(this.clearColor);
|
|
193
|
+
hideForPrepass(scene, this.hidden);
|
|
194
|
+
scene.overrideMaterial = this.normalMaterial;
|
|
195
|
+
scene.background = null;
|
|
196
|
+
renderer.shadowMap.autoUpdate = false;
|
|
197
|
+
renderer.setRenderTarget(target);
|
|
198
|
+
renderer.setClearColor(0x8080ff, 1);
|
|
199
|
+
renderer.clear();
|
|
200
|
+
renderer.render(scene, this.camera);
|
|
201
|
+
renderer.setClearColor(this.clearColor, previousClearAlpha);
|
|
202
|
+
renderer.shadowMap.autoUpdate = previousShadowUpdate;
|
|
203
|
+
scene.background = previousBackground;
|
|
204
|
+
scene.overrideMaterial = previousOverride;
|
|
205
|
+
for (const object of this.hidden)
|
|
206
|
+
object.visible = true;
|
|
207
|
+
this.hidden.length = 0;
|
|
208
|
+
}
|
|
209
|
+
dispose() {
|
|
210
|
+
this.material.dispose();
|
|
211
|
+
this.normalMaterial.dispose();
|
|
212
|
+
this.quad.dispose();
|
|
213
|
+
this.normalTarget?.depthTexture?.dispose();
|
|
214
|
+
this.normalTarget?.dispose();
|
|
215
|
+
}
|
|
216
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import * as THREE from "three";
|
|
1
2
|
import type { DirectionalLightingConfig } from "@jgengine/core/game/playableGame";
|
|
2
3
|
/**
|
|
3
4
|
* Cascaded shadow maps for one directional light — outdoor scenes keep shadows past a
|
|
@@ -11,3 +12,13 @@ import type { DirectionalLightingConfig } from "@jgengine/core/game/playableGame
|
|
|
11
12
|
export declare function CascadedShadows({ entry }: {
|
|
12
13
|
entry: DirectionalLightingConfig;
|
|
13
14
|
}): null;
|
|
15
|
+
/** Structural slice of CSM so the patcher is testable without a renderer. */
|
|
16
|
+
export interface CsmMaterialSetup {
|
|
17
|
+
setupMaterial(material: THREE.Material): void;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Run CSM's `setupMaterial` over every unpatched standard material in the scene,
|
|
21
|
+
* chaining rather than clobbering a material's own `onBeforeCompile`. Called every
|
|
22
|
+
* frame (WeakSet-guarded) so streamed meshes never render lit by all cascades at once.
|
|
23
|
+
*/
|
|
24
|
+
export declare function patchSceneMaterials(scene: THREE.Scene, csm: CsmMaterialSetup, patched: WeakSet<THREE.Material>): void;
|
|
@@ -19,7 +19,6 @@ export function CascadedShadows({ entry }) {
|
|
|
19
19
|
const scene = useThree((s) => s.scene);
|
|
20
20
|
const csmRef = useRef(null);
|
|
21
21
|
const patchedRef = useRef(new WeakSet());
|
|
22
|
-
const frameRef = useRef(0);
|
|
23
22
|
useEffect(() => {
|
|
24
23
|
const cascades = Math.max(2, Math.min(4, Math.floor(entry.cascades ?? 3)));
|
|
25
24
|
const lightDirection = new THREE.Vector3(-entry.position[0], -entry.position[1], -entry.position[2]);
|
|
@@ -47,7 +46,6 @@ export function CascadedShadows({ entry }) {
|
|
|
47
46
|
patchedRef.current = new WeakSet();
|
|
48
47
|
patchSceneMaterials(scene, csm, patchedRef.current);
|
|
49
48
|
csmRef.current = csm;
|
|
50
|
-
frameRef.current = 0;
|
|
51
49
|
return () => {
|
|
52
50
|
csm.dispose();
|
|
53
51
|
csmRef.current = null;
|
|
@@ -71,16 +69,19 @@ export function CascadedShadows({ entry }) {
|
|
|
71
69
|
if (csm === null)
|
|
72
70
|
return;
|
|
73
71
|
csm.camera = camera;
|
|
74
|
-
|
|
75
|
-
//
|
|
76
|
-
|
|
77
|
-
patchSceneMaterials(scene, csm, patchedRef.current);
|
|
78
|
-
}
|
|
72
|
+
// Every frame: a mesh that renders before setupMaterial receives all cascade
|
|
73
|
+
// lights at once (a cascades× sun) — the WeakSet keeps the traverse cheap.
|
|
74
|
+
patchSceneMaterials(scene, csm, patchedRef.current);
|
|
79
75
|
csm.update();
|
|
80
76
|
});
|
|
81
77
|
return null;
|
|
82
78
|
}
|
|
83
|
-
|
|
79
|
+
/**
|
|
80
|
+
* Run CSM's `setupMaterial` over every unpatched standard material in the scene,
|
|
81
|
+
* chaining rather than clobbering a material's own `onBeforeCompile`. Called every
|
|
82
|
+
* frame (WeakSet-guarded) so streamed meshes never render lit by all cascades at once.
|
|
83
|
+
*/
|
|
84
|
+
export function patchSceneMaterials(scene, csm, patched) {
|
|
84
85
|
scene.traverse((obj) => {
|
|
85
86
|
const mesh = obj;
|
|
86
87
|
if (!mesh.isMesh)
|
|
@@ -89,7 +90,17 @@ function patchSceneMaterials(scene, csm, patched) {
|
|
|
89
90
|
for (const mat of materials) {
|
|
90
91
|
if (!isStandardMaterial(mat) || patched.has(mat))
|
|
91
92
|
continue;
|
|
93
|
+
const prior = mat.onBeforeCompile;
|
|
92
94
|
csm.setupMaterial(mat);
|
|
95
|
+
const csmHook = mat.onBeforeCompile;
|
|
96
|
+
if (prior !== THREE.Material.prototype.onBeforeCompile && prior !== csmHook) {
|
|
97
|
+
// setupMaterial assigns onBeforeCompile, which would drop a material's own
|
|
98
|
+
// shader surgery (rim light, detail maps) — run both, the material's first.
|
|
99
|
+
mat.onBeforeCompile = function (shader, renderer) {
|
|
100
|
+
prior.call(this, shader, renderer);
|
|
101
|
+
csmHook.call(this, shader, renderer);
|
|
102
|
+
};
|
|
103
|
+
}
|
|
93
104
|
patched.add(mat);
|
|
94
105
|
}
|
|
95
106
|
});
|
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
import type { EnvironmentSource } from "@jgengine/core/render/environment";
|
|
1
2
|
/** @internal Props for shell {@link EnvironmentLighting}. */
|
|
2
3
|
export interface EnvironmentLightingProps {
|
|
4
|
+
/** Optional authored environment map; absent or `gradient` uses the procedural probe. */
|
|
5
|
+
source?: EnvironmentSource;
|
|
3
6
|
/** IBL intensity. Default 0.35. */
|
|
4
7
|
intensity?: number;
|
|
5
8
|
/** Upper-hemisphere tint for the procedural sky probe. Default cool daylight. */
|
|
@@ -23,4 +26,4 @@ export interface EnvironmentLightingProps {
|
|
|
23
26
|
* Opt out of cinematic IBL with `look: "flat"` on the shell presentation.
|
|
24
27
|
* @internal shell-internal default lighting; games never import it.
|
|
25
28
|
*/
|
|
26
|
-
export declare function EnvironmentLighting({ intensity, skyColor, groundColor, sunDirection, sunColor, }: EnvironmentLightingProps): null;
|
|
29
|
+
export declare function EnvironmentLighting({ source, intensity, skyColor, groundColor, sunDirection, sunColor, }: EnvironmentLightingProps): null;
|