@netforge/engine 0.0.0-stage → 0.1.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/GAME-DESIGN.md +195 -0
- package/LICENSE +21 -0
- package/NOTICE +30 -0
- package/README.md +147 -2
- package/dist/api/appearance.d.ts +37 -0
- package/dist/api/appearance.js +41 -0
- package/dist/api/cards.d.ts +105 -0
- package/dist/api/cards.js +133 -0
- package/dist/api/collector.d.ts +114 -0
- package/dist/api/collector.js +204 -0
- package/dist/api/dungeon.d.ts +58 -0
- package/dist/api/dungeon.js +216 -0
- package/dist/api/harness.d.ts +149 -0
- package/dist/api/harness.js +549 -0
- package/dist/api/idle.d.ts +71 -0
- package/dist/api/idle.js +104 -0
- package/dist/api/race-harness.d.ts +3 -0
- package/dist/api/race-harness.js +148 -0
- package/dist/api/registry.d.ts +241 -0
- package/dist/api/registry.js +394 -0
- package/dist/api/scheduler.d.ts +56 -0
- package/dist/api/scheduler.js +160 -0
- package/dist/api/schemas.d.ts +958 -0
- package/dist/api/schemas.js +859 -0
- package/dist/api/tower.d.ts +127 -0
- package/dist/api/tower.js +183 -0
- package/dist/game/adapter.d.ts +69 -0
- package/dist/game/adapter.js +341 -0
- package/dist/game/arena-bot.d.ts +66 -0
- package/dist/game/arena-bot.js +188 -0
- package/dist/game/bot.d.ts +95 -0
- package/dist/game/bot.js +285 -0
- package/dist/game/compile.d.ts +97 -0
- package/dist/game/compile.js +476 -0
- package/dist/game/director.d.ts +42 -0
- package/dist/game/director.js +179 -0
- package/dist/game/earn-source.d.ts +122 -0
- package/dist/game/earn-source.js +386 -0
- package/dist/game/earn.d.ts +49 -0
- package/dist/game/earn.js +55 -0
- package/dist/game/index.d.ts +37 -0
- package/dist/game/index.js +25 -0
- package/dist/game/platformer.d.ts +156 -0
- package/dist/game/platformer.js +619 -0
- package/dist/game/profile.d.ts +97 -0
- package/dist/game/profile.js +140 -0
- package/dist/game/race.d.ts +177 -0
- package/dist/game/race.js +386 -0
- package/dist/game/racer.d.ts +98 -0
- package/dist/game/racer.js +379 -0
- package/dist/game/runner.d.ts +286 -0
- package/dist/game/runner.js +824 -0
- package/dist/game/threats.d.ts +23 -0
- package/dist/game/threats.js +49 -0
- package/dist/game/view.d.ts +47 -0
- package/dist/game/view.js +123 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.js +24 -0
- package/dist/net/bits.d.ts +33 -0
- package/dist/net/bits.js +135 -0
- package/dist/net/client.d.ts +75 -0
- package/dist/net/client.js +151 -0
- package/dist/net/index.d.ts +23 -0
- package/dist/net/index.js +23 -0
- package/dist/net/input.d.ts +49 -0
- package/dist/net/input.js +112 -0
- package/dist/net/interpolation.d.ts +37 -0
- package/dist/net/interpolation.js +97 -0
- package/dist/net/netsim.d.ts +22 -0
- package/dist/net/netsim.js +52 -0
- package/dist/net/prediction.d.ts +31 -0
- package/dist/net/prediction.js +101 -0
- package/dist/net/profile-client.d.ts +75 -0
- package/dist/net/profile-client.js +173 -0
- package/dist/net/protocol.d.ts +208 -0
- package/dist/net/protocol.js +8 -0
- package/dist/net/room-client.d.ts +57 -0
- package/dist/net/room-client.js +144 -0
- package/dist/net/schema.d.ts +78 -0
- package/dist/net/schema.js +121 -0
- package/dist/net/sim-adapter.d.ts +134 -0
- package/dist/net/sim-adapter.js +22 -0
- package/dist/net/snapshot.d.ts +61 -0
- package/dist/net/snapshot.js +301 -0
- package/dist/rules/adapter.d.ts +53 -0
- package/dist/rules/adapter.js +261 -0
- package/dist/rules/cards.d.ts +115 -0
- package/dist/rules/cards.js +547 -0
- package/dist/rules/collector.d.ts +161 -0
- package/dist/rules/collector.js +732 -0
- package/dist/rules/genres.d.ts +13 -0
- package/dist/rules/genres.js +38 -0
- package/dist/rules/harness.d.ts +16 -0
- package/dist/rules/harness.js +127 -0
- package/dist/rules/idle.d.ts +115 -0
- package/dist/rules/idle.js +370 -0
- package/dist/rules/index.d.ts +16 -0
- package/dist/rules/index.js +14 -0
- package/dist/rules/kit.d.ts +171 -0
- package/dist/rules/kit.js +88 -0
- package/dist/rules/tower.d.ts +214 -0
- package/dist/rules/tower.js +655 -0
- package/dist/runtime/arena.d.ts +86 -0
- package/dist/runtime/arena.js +269 -0
- package/dist/runtime/assets.d.ts +26 -0
- package/dist/runtime/assets.js +35 -0
- package/dist/runtime/camera.d.ts +113 -0
- package/dist/runtime/camera.js +247 -0
- package/dist/runtime/crowd.d.ts +18 -0
- package/dist/runtime/crowd.js +62 -0
- package/dist/runtime/feel/audio.d.ts +150 -0
- package/dist/runtime/feel/audio.js +327 -0
- package/dist/runtime/feel/clock.d.ts +52 -0
- package/dist/runtime/feel/clock.js +80 -0
- package/dist/runtime/feel/flash.d.ts +28 -0
- package/dist/runtime/feel/flash.js +60 -0
- package/dist/runtime/feel/index.d.ts +27 -0
- package/dist/runtime/feel/index.js +16 -0
- package/dist/runtime/feel/kit.d.ts +159 -0
- package/dist/runtime/feel/kit.js +226 -0
- package/dist/runtime/feel/knockback.d.ts +41 -0
- package/dist/runtime/feel/knockback.js +70 -0
- package/dist/runtime/feel/noise.d.ts +6 -0
- package/dist/runtime/feel/noise.js +30 -0
- package/dist/runtime/feel/numbers.d.ts +91 -0
- package/dist/runtime/feel/numbers.js +172 -0
- package/dist/runtime/feel/rumble.d.ts +40 -0
- package/dist/runtime/feel/rumble.js +58 -0
- package/dist/runtime/feel/sound.d.ts +96 -0
- package/dist/runtime/feel/sound.js +176 -0
- package/dist/runtime/feel/spring.d.ts +42 -0
- package/dist/runtime/feel/spring.js +95 -0
- package/dist/runtime/feel/trauma.d.ts +48 -0
- package/dist/runtime/feel/trauma.js +76 -0
- package/dist/runtime/feel/tween.d.ts +66 -0
- package/dist/runtime/feel/tween.js +180 -0
- package/dist/runtime/feel/types.d.ts +30 -0
- package/dist/runtime/feel/types.js +5 -0
- package/dist/runtime/index.d.ts +28 -0
- package/dist/runtime/index.js +26 -0
- package/dist/runtime/input.d.ts +38 -0
- package/dist/runtime/input.js +159 -0
- package/dist/runtime/interpolate.d.ts +6 -0
- package/dist/runtime/interpolate.js +13 -0
- package/dist/runtime/render/animation.d.ts +63 -0
- package/dist/runtime/render/animation.js +128 -0
- package/dist/runtime/render/atmosphere.d.ts +32 -0
- package/dist/runtime/render/atmosphere.js +59 -0
- package/dist/runtime/render/crowd-motion.d.ts +68 -0
- package/dist/runtime/render/crowd-motion.js +163 -0
- package/dist/runtime/render/decals.d.ts +64 -0
- package/dist/runtime/render/decals.js +222 -0
- package/dist/runtime/render/highlight.d.ts +29 -0
- package/dist/runtime/render/highlight.js +110 -0
- package/dist/runtime/render/index.d.ts +35 -0
- package/dist/runtime/render/index.js +22 -0
- package/dist/runtime/render/lighting.d.ts +50 -0
- package/dist/runtime/render/lighting.js +142 -0
- package/dist/runtime/render/particles.d.ts +117 -0
- package/dist/runtime/render/particles.js +289 -0
- package/dist/runtime/render/post.d.ts +92 -0
- package/dist/runtime/render/post.js +191 -0
- package/dist/runtime/render/presets.d.ts +57 -0
- package/dist/runtime/render/presets.js +116 -0
- package/dist/runtime/render/quality.d.ts +113 -0
- package/dist/runtime/render/quality.js +180 -0
- package/dist/runtime/render/rig.d.ts +29 -0
- package/dist/runtime/render/rig.js +107 -0
- package/dist/runtime/render/sprite.d.ts +34 -0
- package/dist/runtime/render/sprite.js +58 -0
- package/dist/runtime/render/stack.d.ts +74 -0
- package/dist/runtime/render/stack.js +132 -0
- package/dist/runtime/render/vat.d.ts +78 -0
- package/dist/runtime/render/vat.js +219 -0
- package/dist/runtime/renderer.d.ts +19 -0
- package/dist/runtime/renderer.js +18 -0
- package/dist/runtime/stats.d.ts +11 -0
- package/dist/runtime/stats.js +29 -0
- package/dist/runtime/styles/handdrawn/ground.d.ts +11 -0
- package/dist/runtime/styles/handdrawn/ground.js +282 -0
- package/dist/runtime/styles/handdrawn/index.d.ts +4 -0
- package/dist/runtime/styles/handdrawn/index.js +94 -0
- package/dist/runtime/styles/handdrawn/ink.d.ts +31 -0
- package/dist/runtime/styles/handdrawn/ink.js +68 -0
- package/dist/runtime/styles/handdrawn/looks.d.ts +18 -0
- package/dist/runtime/styles/handdrawn/looks.js +42 -0
- package/dist/runtime/styles/handdrawn/paint.d.ts +59 -0
- package/dist/runtime/styles/handdrawn/paint.js +129 -0
- package/dist/runtime/styles/handdrawn/palette.d.ts +49 -0
- package/dist/runtime/styles/handdrawn/palette.js +70 -0
- package/dist/runtime/styles/handdrawn/textures.d.ts +27 -0
- package/dist/runtime/styles/handdrawn/textures.js +150 -0
- package/dist/runtime/styles/handdrawn/vfx.d.ts +14 -0
- package/dist/runtime/styles/handdrawn/vfx.js +73 -0
- package/dist/runtime/styles/index.d.ts +9 -0
- package/dist/runtime/styles/index.js +32 -0
- package/dist/runtime/styles/kit.d.ts +193 -0
- package/dist/runtime/styles/kit.js +1 -0
- package/dist/runtime/styles/pixel/index.d.ts +46 -0
- package/dist/runtime/styles/pixel/index.js +167 -0
- package/dist/runtime/styles/ps1/index.d.ts +29 -0
- package/dist/runtime/styles/ps1/index.js +41 -0
- package/dist/runtime/styles/ps1/lights.d.ts +47 -0
- package/dist/runtime/styles/ps1/lights.js +146 -0
- package/dist/runtime/styles/ps1/materials.d.ts +62 -0
- package/dist/runtime/styles/ps1/materials.js +285 -0
- package/dist/runtime/styles/ps1/pipeline.d.ts +52 -0
- package/dist/runtime/styles/ps1/pipeline.js +111 -0
- package/dist/runtime/styles/ps1/surfaces.d.ts +44 -0
- package/dist/runtime/styles/ps1/surfaces.js +228 -0
- package/dist/runtime/styles/toon/index.d.ts +5 -0
- package/dist/runtime/styles/toon/index.js +178 -0
- package/dist/runtime/styles/toon/ink.d.ts +39 -0
- package/dist/runtime/styles/toon/ink.js +73 -0
- package/dist/runtime/styles/toon/look.d.ts +10 -0
- package/dist/runtime/styles/toon/look.js +112 -0
- package/dist/runtime/styles/toon/material.d.ts +74 -0
- package/dist/runtime/styles/toon/material.js +208 -0
- package/dist/runtime/styles/toon/normals.d.ts +26 -0
- package/dist/runtime/styles/toon/normals.js +127 -0
- package/dist/runtime/styles/toon/shadows.d.ts +24 -0
- package/dist/runtime/styles/toon/shadows.js +42 -0
- package/dist/runtime/styles/toon/vfx.d.ts +15 -0
- package/dist/runtime/styles/toon/vfx.js +47 -0
- package/dist/runtime/styles/view.d.ts +35 -0
- package/dist/runtime/styles/view.js +34 -0
- package/dist/runtime/styles/voxel/core.d.ts +114 -0
- package/dist/runtime/styles/voxel/core.js +577 -0
- package/dist/runtime/styles/voxel/geometry.d.ts +27 -0
- package/dist/runtime/styles/voxel/geometry.js +40 -0
- package/dist/runtime/styles/voxel/index.d.ts +21 -0
- package/dist/runtime/styles/voxel/index.js +23 -0
- package/dist/runtime/styles/voxel/kit.d.ts +6 -0
- package/dist/runtime/styles/voxel/kit.js +11 -0
- package/dist/runtime/styles/voxel/look.d.ts +10 -0
- package/dist/runtime/styles/voxel/look.js +16 -0
- package/dist/runtime/styles/voxel/props.d.ts +9 -0
- package/dist/runtime/styles/voxel/props.js +52 -0
- package/dist/runtime/styles/voxel/shapes.d.ts +41 -0
- package/dist/runtime/styles/voxel/shapes.js +83 -0
- package/dist/runtime/styles/voxel/world.d.ts +49 -0
- package/dist/runtime/styles/voxel/world.js +422 -0
- package/llms.txt +183 -0
- package/package.json +82 -3
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { AdditiveBlending, DoubleSide, DynamicDrawUsage, InstancedBufferAttribute, InstancedBufferGeometry, Mesh, MeshBasicNodeMaterial, NormalBlending, PlaneGeometry, } from "three/webgpu";
|
|
2
|
+
import { abs, atan, clamp, cos, float, instancedBufferAttribute, length, max, mix, positionGeometry, pow, select, sin, smoothstep, step, uniform, uv, vec3, vec4, } from "three/tsl";
|
|
3
|
+
import { ParticleRing } from "./particles.js";
|
|
4
|
+
const DECAL_INDEX = { scorch: 0, splat: 1, ring: 2 };
|
|
5
|
+
function quadGeometry(capacity) {
|
|
6
|
+
const quad = new PlaneGeometry(1, 1);
|
|
7
|
+
const g = new InstancedBufferGeometry();
|
|
8
|
+
g.index = quad.index;
|
|
9
|
+
g.setAttribute("position", quad.getAttribute("position"));
|
|
10
|
+
g.setAttribute("uv", quad.getAttribute("uv"));
|
|
11
|
+
g.instanceCount = capacity;
|
|
12
|
+
return g;
|
|
13
|
+
}
|
|
14
|
+
function vec4Attr(capacity) {
|
|
15
|
+
const a = new InstancedBufferAttribute(new Float32Array(capacity * 4), 4);
|
|
16
|
+
a.setUsage(DynamicDrawUsage);
|
|
17
|
+
return a;
|
|
18
|
+
}
|
|
19
|
+
function upload(ring, attrs) {
|
|
20
|
+
const ranges = ring.flush();
|
|
21
|
+
if (ranges.length === 0)
|
|
22
|
+
return;
|
|
23
|
+
for (const a of attrs) {
|
|
24
|
+
a.clearUpdateRanges();
|
|
25
|
+
for (const [s, c] of ranges)
|
|
26
|
+
a.addUpdateRange(s * 4, c * 4);
|
|
27
|
+
a.needsUpdate = true;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
const hexRgb = (hex) => {
|
|
31
|
+
const r = ((hex >> 16) & 255) / 255;
|
|
32
|
+
const g = ((hex >> 8) & 255) / 255;
|
|
33
|
+
const b = (hex & 255) / 255;
|
|
34
|
+
return [r * r, g * g, b * b];
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* Ground decals (scorch marks, splats, rings) in one draw: flat quads on
|
|
38
|
+
* the floor, shapes drawn procedurally in the fragment stage (no textures),
|
|
39
|
+
* faded over their life. A ring buffer: the oldest decal is reused.
|
|
40
|
+
*/
|
|
41
|
+
export class GroundDecals {
|
|
42
|
+
time = uniform(0);
|
|
43
|
+
ring;
|
|
44
|
+
mesh;
|
|
45
|
+
a;
|
|
46
|
+
b;
|
|
47
|
+
c;
|
|
48
|
+
rand;
|
|
49
|
+
/** Height above the floor, m. */
|
|
50
|
+
floorY = 0.012;
|
|
51
|
+
constructor(parent, capacity = 256, random = Math.random, look) {
|
|
52
|
+
this.ring = new ParticleRing(capacity);
|
|
53
|
+
this.rand = random;
|
|
54
|
+
this.a = vec4Attr(capacity); // x, z, rotation, size
|
|
55
|
+
this.b = vec4Attr(capacity); // t0, life, kind, opacity
|
|
56
|
+
this.c = vec4Attr(capacity); // r, g, b, seed
|
|
57
|
+
for (let i = 0; i < capacity; i++)
|
|
58
|
+
this.b.array[i * 4 + 1] = 0;
|
|
59
|
+
const A = instancedBufferAttribute(this.a, "vec4");
|
|
60
|
+
const B = instancedBufferAttribute(this.b, "vec4");
|
|
61
|
+
const C = instancedBufferAttribute(this.c, "vec4");
|
|
62
|
+
const age = this.time.sub(B.x);
|
|
63
|
+
const life = max(B.y, float(1e-4));
|
|
64
|
+
const t = clamp(age.div(life), 0, 1);
|
|
65
|
+
const alive = step(0, age).mul(step(age, life));
|
|
66
|
+
const pop = smoothstep(0, 0.12, age).mul(0.25).add(0.75);
|
|
67
|
+
const size = A.w.mul(pop).mul(alive);
|
|
68
|
+
const cs = cos(A.z);
|
|
69
|
+
const sn = sin(A.z);
|
|
70
|
+
const cx = positionGeometry.x.mul(size);
|
|
71
|
+
const cz = positionGeometry.y.mul(size);
|
|
72
|
+
const mat = new MeshBasicNodeMaterial();
|
|
73
|
+
mat.positionNode = vec3(A.x.add(cx.mul(cs)).sub(cz.mul(sn)), float(this.floorY), A.y.add(cx.mul(sn)).add(cz.mul(cs)));
|
|
74
|
+
const p = uv().sub(0.5);
|
|
75
|
+
const r = length(p).mul(2);
|
|
76
|
+
const ang = atan(p.y, p.x).add(C.w);
|
|
77
|
+
const scorchEdge = float(0.78).add(sin(ang.mul(7)).mul(0.1)).add(sin(ang.mul(13).add(1.7)).mul(0.05));
|
|
78
|
+
const scorch = smoothstep(scorchEdge, scorchEdge.mul(0.35), r);
|
|
79
|
+
const lobes = pow(max(sin(ang.mul(5)), float(0)), float(3));
|
|
80
|
+
const splatEdge = float(0.5).add(lobes.mul(0.32)).add(sin(ang.mul(11).add(0.6)).mul(0.07));
|
|
81
|
+
const splat = smoothstep(splatEdge, splatEdge.sub(0.07), r);
|
|
82
|
+
const ringA = smoothstep(0.1, 0.0, abs(r.sub(0.82)));
|
|
83
|
+
const shape = select(B.z.lessThan(0.5), scorch, select(B.z.lessThan(1.5), splat, ringA));
|
|
84
|
+
const fade = float(1).sub(smoothstep(0.55, 1, t));
|
|
85
|
+
mat.colorNode = vec4(C.xyz, 1);
|
|
86
|
+
mat.opacityNode = shape.mul(fade).mul(B.w).mul(alive);
|
|
87
|
+
// An art style's decal look replaces the shading (same quads, same draw).
|
|
88
|
+
const styled = look?.({ uv: uv(), r, angle: ang, kind: B.z, t, color: C.xyz, shape, opacity: B.w.mul(alive) });
|
|
89
|
+
if (styled) {
|
|
90
|
+
mat.colorNode = vec4(styled.color, 1);
|
|
91
|
+
mat.opacityNode = styled.opacity;
|
|
92
|
+
}
|
|
93
|
+
mat.transparent = true;
|
|
94
|
+
mat.depthWrite = false;
|
|
95
|
+
mat.polygonOffset = true;
|
|
96
|
+
mat.polygonOffsetFactor = -2;
|
|
97
|
+
mat.polygonOffsetUnits = -2;
|
|
98
|
+
mat.blending = NormalBlending;
|
|
99
|
+
mat.side = DoubleSide;
|
|
100
|
+
this.mesh = new Mesh(quadGeometry(capacity), mat);
|
|
101
|
+
this.mesh.frustumCulled = false;
|
|
102
|
+
this.mesh.renderOrder = 1;
|
|
103
|
+
this.mesh.receiveShadow = false;
|
|
104
|
+
this.mesh.name = "GroundDecals";
|
|
105
|
+
parent.add(this.mesh);
|
|
106
|
+
}
|
|
107
|
+
/** Stamp a decal on the floor at (x, z). */
|
|
108
|
+
add(kind, x, z, opts = {}) {
|
|
109
|
+
const [s] = this.ring.alloc(1);
|
|
110
|
+
if (s === undefined)
|
|
111
|
+
return;
|
|
112
|
+
const o = s * 4;
|
|
113
|
+
const A = this.a.array;
|
|
114
|
+
const B = this.b.array;
|
|
115
|
+
const C = this.c.array;
|
|
116
|
+
A[o] = x;
|
|
117
|
+
A[o + 1] = z;
|
|
118
|
+
A[o + 2] = opts.rotation ?? this.rand() * Math.PI * 2;
|
|
119
|
+
A[o + 3] = opts.size ?? 1.6;
|
|
120
|
+
B[o] = this.time.value;
|
|
121
|
+
B[o + 1] = opts.life ?? 6;
|
|
122
|
+
B[o + 2] = DECAL_INDEX[kind];
|
|
123
|
+
B[o + 3] = opts.opacity ?? 0.75;
|
|
124
|
+
const [r, g, b] = hexRgb(opts.color ?? (kind === "scorch" ? 0x0a0806 : 0x6a2c08));
|
|
125
|
+
const glow = opts.glow ?? 1;
|
|
126
|
+
C[o] = r * glow;
|
|
127
|
+
C[o + 1] = g * glow;
|
|
128
|
+
C[o + 2] = b * glow;
|
|
129
|
+
C[o + 3] = this.rand() * 6.283;
|
|
130
|
+
}
|
|
131
|
+
update(timeSeconds) {
|
|
132
|
+
this.time.value = timeSeconds;
|
|
133
|
+
upload(this.ring, [this.a, this.b, this.c]);
|
|
134
|
+
}
|
|
135
|
+
dispose() {
|
|
136
|
+
this.mesh.removeFromParent();
|
|
137
|
+
this.mesh.geometry.dispose();
|
|
138
|
+
this.mesh.material.dispose();
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Expanding shockwave rings on the floor (nova blasts, landings, dash
|
|
143
|
+
* starts): one additive draw, pooled. Generalized from EMBERWATCH's rings.
|
|
144
|
+
*/
|
|
145
|
+
export class GroundRings {
|
|
146
|
+
time = uniform(0);
|
|
147
|
+
ring;
|
|
148
|
+
mesh;
|
|
149
|
+
a;
|
|
150
|
+
b;
|
|
151
|
+
/** Rings fired so far (playtest checks). */
|
|
152
|
+
fired = 0;
|
|
153
|
+
constructor(parent, capacity = 32, look) {
|
|
154
|
+
this.ring = new ParticleRing(capacity);
|
|
155
|
+
this.a = vec4Attr(capacity); // x, z, t0, radius
|
|
156
|
+
this.b = vec4Attr(capacity); // r, g, b, duration
|
|
157
|
+
for (let i = 0; i < capacity; i++)
|
|
158
|
+
this.a.array[i * 4 + 2] = -1e6;
|
|
159
|
+
const A = instancedBufferAttribute(this.a, "vec4");
|
|
160
|
+
const B = instancedBufferAttribute(this.b, "vec4");
|
|
161
|
+
const age = this.time.sub(A.z);
|
|
162
|
+
const dur = max(B.w, float(1e-4));
|
|
163
|
+
const t = clamp(age.div(dur), 0, 1);
|
|
164
|
+
const alive = step(0, age).mul(step(age, dur));
|
|
165
|
+
const ease = float(1).sub(pow(float(1).sub(t), float(3)));
|
|
166
|
+
const scale = A.w.mul(mix(float(0.3), float(1), ease)).mul(2).mul(alive);
|
|
167
|
+
const mat = new MeshBasicNodeMaterial();
|
|
168
|
+
mat.positionNode = vec3(A.x.add(positionGeometry.x.mul(scale)), float(0.05), A.y.add(positionGeometry.y.mul(scale)));
|
|
169
|
+
const r = length(uv().sub(0.5)).mul(2);
|
|
170
|
+
const width = mix(float(0.22), float(0.06), t);
|
|
171
|
+
const band = smoothstep(float(1).sub(width), float(0.97), r).mul(smoothstep(1.0, 0.97, r));
|
|
172
|
+
const inner = smoothstep(0.2, 1, r).mul(0.18).mul(step(r, float(1)));
|
|
173
|
+
mat.colorNode = vec4(B.xyz, 1);
|
|
174
|
+
mat.opacityNode = band.add(inner).mul(float(1).sub(t)).mul(alive);
|
|
175
|
+
mat.transparent = true;
|
|
176
|
+
mat.depthWrite = false;
|
|
177
|
+
mat.blending = AdditiveBlending;
|
|
178
|
+
const styled = look?.({ uv: uv(), r, t, alive, color: B.xyz });
|
|
179
|
+
if (styled) {
|
|
180
|
+
mat.colorNode = vec4(styled.color, 1);
|
|
181
|
+
mat.opacityNode = styled.opacity;
|
|
182
|
+
if (styled.blending)
|
|
183
|
+
mat.blending = styled.blending === "add" ? AdditiveBlending : NormalBlending;
|
|
184
|
+
}
|
|
185
|
+
mat.side = DoubleSide;
|
|
186
|
+
mat.fog = false;
|
|
187
|
+
this.mesh = new Mesh(quadGeometry(capacity), mat);
|
|
188
|
+
this.mesh.frustumCulled = false;
|
|
189
|
+
this.mesh.renderOrder = 15;
|
|
190
|
+
this.mesh.name = "GroundRings";
|
|
191
|
+
parent.add(this.mesh);
|
|
192
|
+
}
|
|
193
|
+
/** A ring at (x, z) reaching `radius` m. */
|
|
194
|
+
fire(x, z, radius, opts = {}) {
|
|
195
|
+
const [s] = this.ring.alloc(1);
|
|
196
|
+
if (s === undefined)
|
|
197
|
+
return;
|
|
198
|
+
this.fired++;
|
|
199
|
+
const o = s * 4;
|
|
200
|
+
const A = this.a.array;
|
|
201
|
+
const B = this.b.array;
|
|
202
|
+
A[o] = x;
|
|
203
|
+
A[o + 1] = z;
|
|
204
|
+
A[o + 2] = this.time.value;
|
|
205
|
+
A[o + 3] = radius;
|
|
206
|
+
const [r, g, b] = hexRgb(opts.color ?? 0xffb347);
|
|
207
|
+
const glow = opts.glow ?? 1.6;
|
|
208
|
+
B[o] = r * glow;
|
|
209
|
+
B[o + 1] = g * glow;
|
|
210
|
+
B[o + 2] = b * glow;
|
|
211
|
+
B[o + 3] = opts.duration ?? 0.32;
|
|
212
|
+
}
|
|
213
|
+
update(timeSeconds) {
|
|
214
|
+
this.time.value = timeSeconds;
|
|
215
|
+
upload(this.ring, [this.a, this.b]);
|
|
216
|
+
}
|
|
217
|
+
dispose() {
|
|
218
|
+
this.mesh.removeFromParent();
|
|
219
|
+
this.mesh.geometry.dispose();
|
|
220
|
+
this.mesh.material.dispose();
|
|
221
|
+
}
|
|
222
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { Mesh } from "three/webgpu";
|
|
2
|
+
import type { ColorRepresentation, Object3D } from "three/webgpu";
|
|
3
|
+
export interface HeroHighlightOptions {
|
|
4
|
+
/** Rim glow colour, or null for none. Default warm white. */
|
|
5
|
+
readonly rim?: ColorRepresentation | null;
|
|
6
|
+
/** Rim brightness. Default 1.6 (bloom picks it up above 1). */
|
|
7
|
+
readonly rimStrength?: number;
|
|
8
|
+
/** Rim falloff exponent: higher is a thinner edge. Default 2.4. */
|
|
9
|
+
readonly rimPower?: number;
|
|
10
|
+
/** Outline colour, or null for none. Default near-black. */
|
|
11
|
+
readonly outline?: ColorRepresentation | null;
|
|
12
|
+
/** Outline thickness in metres (world). Default 0.03. */
|
|
13
|
+
readonly outlineWidth?: number;
|
|
14
|
+
}
|
|
15
|
+
export declare class HeroHighlight {
|
|
16
|
+
readonly root: Object3D;
|
|
17
|
+
/** The extra draws (outline hulls and rim shells), for tests and counting. */
|
|
18
|
+
readonly parts: Mesh[];
|
|
19
|
+
private readonly rimStrength;
|
|
20
|
+
private base;
|
|
21
|
+
constructor(root: Object3D, opts?: HeroHighlightOptions);
|
|
22
|
+
/** A draw of `mesh`'s geometry (and skeleton) with another material, as its child at identity. */
|
|
23
|
+
private extra;
|
|
24
|
+
/** 0..1 fade on top of the rim strength and outline (0 hides both). */
|
|
25
|
+
set strength(v: number);
|
|
26
|
+
/** Show or hide every extra draw. */
|
|
27
|
+
set visible(v: boolean);
|
|
28
|
+
dispose(): void;
|
|
29
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Make one character pop off the scene (the local hero, a selected unit):
|
|
3
|
+
* an inverted-hull outline and an additive rim glow, both as extra draws
|
|
4
|
+
* that share each mesh's geometry (and skeleton, so they follow the
|
|
5
|
+
* animation). The character's own materials are never touched, so a model
|
|
6
|
+
* shared with teammates stays as it was.
|
|
7
|
+
*
|
|
8
|
+
* const hl = new HeroHighlight(character.root, { rim: "#ffe6a8", outline: "#1a0f05" });
|
|
9
|
+
* hl.strength = 0.5; // fade it (iframes, downed)
|
|
10
|
+
* hl.dispose(); // remove the extra draws
|
|
11
|
+
*/
|
|
12
|
+
import { isSprite } from "./sprite.js";
|
|
13
|
+
import { AdditiveBlending, BackSide, Color, FrontSide, Mesh, MeshBasicNodeMaterial, SkinnedMesh } from "three/webgpu";
|
|
14
|
+
import { abs, dot, float, normalLocal, normalView, positionLocal, positionViewDirection, pow, uniform, vec3 } from "three/tsl";
|
|
15
|
+
/** Product of the scales from `mesh` up to and including `root` (world size of one local unit). */
|
|
16
|
+
function scaleTo(mesh, root) {
|
|
17
|
+
let s = 1;
|
|
18
|
+
for (let o = mesh; o; o = o.parent) {
|
|
19
|
+
s *= Math.max(Math.abs(o.scale.x), Math.abs(o.scale.y), Math.abs(o.scale.z)) || 1;
|
|
20
|
+
if (o === root)
|
|
21
|
+
break;
|
|
22
|
+
}
|
|
23
|
+
return s;
|
|
24
|
+
}
|
|
25
|
+
export class HeroHighlight {
|
|
26
|
+
root;
|
|
27
|
+
/** The extra draws (outline hulls and rim shells), for tests and counting. */
|
|
28
|
+
parts = [];
|
|
29
|
+
rimStrength = uniform(1);
|
|
30
|
+
base;
|
|
31
|
+
constructor(root, opts = {}) {
|
|
32
|
+
this.root = root;
|
|
33
|
+
const rim = opts.rim === undefined ? "#fff1cc" : opts.rim;
|
|
34
|
+
const outline = opts.outline === undefined ? "#120a04" : opts.outline;
|
|
35
|
+
const width = opts.outlineWidth ?? 0.03;
|
|
36
|
+
this.base = opts.rimStrength ?? 1.6;
|
|
37
|
+
this.rimStrength.value = this.base;
|
|
38
|
+
const meshes = [];
|
|
39
|
+
root.traverse((o) => {
|
|
40
|
+
// Sprites (render/sprite.ts) draw their own 1 px rim; a hull around a flat quad would cover it.
|
|
41
|
+
if (o.isMesh && !isSprite(o.geometry))
|
|
42
|
+
meshes.push(o);
|
|
43
|
+
});
|
|
44
|
+
const outlineColor = outline === null ? null : new Color(outline);
|
|
45
|
+
const rimColor = rim === null ? null : new Color(rim);
|
|
46
|
+
const facing = abs(dot(normalView, positionViewDirection));
|
|
47
|
+
const edge = pow(float(1).sub(facing), opts.rimPower ?? 2.4);
|
|
48
|
+
for (const mesh of meshes) {
|
|
49
|
+
// Widths are world metres; each mesh inflates in its own local units.
|
|
50
|
+
const unit = scaleTo(mesh, root);
|
|
51
|
+
const made = [];
|
|
52
|
+
if (outlineColor) {
|
|
53
|
+
const m = new MeshBasicNodeMaterial({ side: BackSide });
|
|
54
|
+
m.colorNode = vec3(outlineColor.r, outlineColor.g, outlineColor.b);
|
|
55
|
+
m.positionNode = positionLocal.add(normalLocal.mul(float(width / unit)));
|
|
56
|
+
made.push([this.extra(mesh, m), "HeroOutline"]);
|
|
57
|
+
}
|
|
58
|
+
if (rimColor) {
|
|
59
|
+
const m = new MeshBasicNodeMaterial({ side: FrontSide, transparent: true, depthWrite: false, blending: AdditiveBlending });
|
|
60
|
+
m.colorNode = vec3(rimColor.r, rimColor.g, rimColor.b).mul(edge).mul(this.rimStrength);
|
|
61
|
+
m.positionNode = positionLocal.add(normalLocal.mul(float(0.003 / unit)));
|
|
62
|
+
made.push([this.extra(mesh, m), "HeroRim"]);
|
|
63
|
+
}
|
|
64
|
+
for (const [extra, name] of made) {
|
|
65
|
+
extra.name = name;
|
|
66
|
+
// The outline draws before the body, the rim after it.
|
|
67
|
+
extra.renderOrder = name === "HeroOutline" ? -1 : 1;
|
|
68
|
+
mesh.add(extra);
|
|
69
|
+
this.parts.push(extra);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/** A draw of `mesh`'s geometry (and skeleton) with another material, as its child at identity. */
|
|
74
|
+
extra(mesh, material) {
|
|
75
|
+
let extra;
|
|
76
|
+
if (mesh.isSkinnedMesh === true) {
|
|
77
|
+
const s = mesh;
|
|
78
|
+
const e = new SkinnedMesh(mesh.geometry, material);
|
|
79
|
+
e.bindMode = s.bindMode;
|
|
80
|
+
e.bind(s.skeleton, s.bindMatrix);
|
|
81
|
+
extra = e;
|
|
82
|
+
}
|
|
83
|
+
else
|
|
84
|
+
extra = new Mesh(mesh.geometry, material);
|
|
85
|
+
extra.castShadow = false;
|
|
86
|
+
extra.receiveShadow = false;
|
|
87
|
+
extra.frustumCulled = false;
|
|
88
|
+
return extra;
|
|
89
|
+
}
|
|
90
|
+
/** 0..1 fade on top of the rim strength and outline (0 hides both). */
|
|
91
|
+
set strength(v) {
|
|
92
|
+
const k = Math.max(0, Math.min(1, v));
|
|
93
|
+
this.rimStrength.value = this.base * k;
|
|
94
|
+
for (const p of this.parts)
|
|
95
|
+
if (p.name === "HeroOutline")
|
|
96
|
+
p.visible = k > 0.05;
|
|
97
|
+
}
|
|
98
|
+
/** Show or hide every extra draw. */
|
|
99
|
+
set visible(v) {
|
|
100
|
+
for (const p of this.parts)
|
|
101
|
+
p.visible = v;
|
|
102
|
+
}
|
|
103
|
+
dispose() {
|
|
104
|
+
for (const p of this.parts) {
|
|
105
|
+
p.removeFromParent();
|
|
106
|
+
p.material.dispose();
|
|
107
|
+
}
|
|
108
|
+
this.parts.length = 0;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @netforge/engine/runtime render module: lighting
|
|
3
|
+
* with a following shadow, post-processing (bloom, grade, tone mapping,
|
|
4
|
+
* FXAA, outline), atmosphere, quality tiers and dynamic resolution, GPU
|
|
5
|
+
* particles, ground decals and rings, skinned and VAT-animated characters,
|
|
6
|
+
* and `createRenderStack`, which wires them in one call.
|
|
7
|
+
* WebGPU first, WebGL2 fallback: everything here runs on both backends.
|
|
8
|
+
*/
|
|
9
|
+
export { createRenderStack } from "./stack.js";
|
|
10
|
+
export type { RenderStack, RenderStackOptions } from "./stack.js";
|
|
11
|
+
export { LightingRig, snapToTexel } from "./lighting.js";
|
|
12
|
+
export type { LightingOptions } from "./lighting.js";
|
|
13
|
+
export { PostFX } from "./post.js";
|
|
14
|
+
export type { PostFXOptions } from "./post.js";
|
|
15
|
+
export { Atmosphere } from "./atmosphere.js";
|
|
16
|
+
export type { AtmosphereOptions } from "./atmosphere.js";
|
|
17
|
+
export { QualityManager, DynamicResolution, detectTier, browserHints, qualitySettings, QUALITY_TIERS } from "./quality.js";
|
|
18
|
+
export type { QualityTier, QualitySettings, DeviceHints, DynamicResolutionOptions } from "./quality.js";
|
|
19
|
+
export { GpuParticles, ParticleRing, PARTICLE_PRESETS } from "./particles.js";
|
|
20
|
+
export type { ParticleBurstOptions as GpuBurstOptions, ParticlePreset, ParticlePresetName, GpuParticlesOptions, ParticleShade, ParticleShadeInput, ParticleShadeOutput } from "./particles.js";
|
|
21
|
+
export { GroundDecals, GroundRings } from "./decals.js";
|
|
22
|
+
export type { DecalKind, DecalOptions, RingOptions } from "./decals.js";
|
|
23
|
+
export { AnimatedCharacter, locomotionState, DEFAULT_LOCOMOTION } from "./animation.js";
|
|
24
|
+
export type { AnimatedCharacterOptions, LocomotionState, LocomotionThresholds, PlayOptions as AnimationPlayOptions } from "./animation.js";
|
|
25
|
+
export { bakeVertexAnimation, VatCrowd } from "./vat.js";
|
|
26
|
+
export type { VertexAnimation, VatClip, BakeOptions, VatCrowdOptions } from "./vat.js";
|
|
27
|
+
export { applyCrowdMotion, AnimatedCrowd, toNodeMaterial } from "./crowd-motion.js";
|
|
28
|
+
export type { CrowdMotionOptions, CrowdAttributes } from "./crowd-motion.js";
|
|
29
|
+
export { billboardWorld, isSprite, markSprite, markSpriteMaterial, SPRITE_TEXEL, spriteBillboardNode, spriteScale } from "./sprite.js";
|
|
30
|
+
export { HeroHighlight } from "./highlight.js";
|
|
31
|
+
export type { HeroHighlightOptions } from "./highlight.js";
|
|
32
|
+
export { buildSegmentRig } from "./rig.js";
|
|
33
|
+
export type { SegmentRig, SegmentRigOptions } from "./rig.js";
|
|
34
|
+
export { resolveLook, LOOK_PRESETS } from "./presets.js";
|
|
35
|
+
export type { Look, LookPreset, LightingPreset, AtmospherePreset, GradePreset } from "./presets.js";
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @netforge/engine/runtime render module: lighting
|
|
3
|
+
* with a following shadow, post-processing (bloom, grade, tone mapping,
|
|
4
|
+
* FXAA, outline), atmosphere, quality tiers and dynamic resolution, GPU
|
|
5
|
+
* particles, ground decals and rings, skinned and VAT-animated characters,
|
|
6
|
+
* and `createRenderStack`, which wires them in one call.
|
|
7
|
+
* WebGPU first, WebGL2 fallback: everything here runs on both backends.
|
|
8
|
+
*/
|
|
9
|
+
export { createRenderStack } from "./stack.js";
|
|
10
|
+
export { LightingRig, snapToTexel } from "./lighting.js";
|
|
11
|
+
export { PostFX } from "./post.js";
|
|
12
|
+
export { Atmosphere } from "./atmosphere.js";
|
|
13
|
+
export { QualityManager, DynamicResolution, detectTier, browserHints, qualitySettings, QUALITY_TIERS } from "./quality.js";
|
|
14
|
+
export { GpuParticles, ParticleRing, PARTICLE_PRESETS } from "./particles.js";
|
|
15
|
+
export { GroundDecals, GroundRings } from "./decals.js";
|
|
16
|
+
export { AnimatedCharacter, locomotionState, DEFAULT_LOCOMOTION } from "./animation.js";
|
|
17
|
+
export { bakeVertexAnimation, VatCrowd } from "./vat.js";
|
|
18
|
+
export { applyCrowdMotion, AnimatedCrowd, toNodeMaterial } from "./crowd-motion.js";
|
|
19
|
+
export { billboardWorld, isSprite, markSprite, markSpriteMaterial, SPRITE_TEXEL, spriteBillboardNode, spriteScale } from "./sprite.js";
|
|
20
|
+
export { HeroHighlight } from "./highlight.js";
|
|
21
|
+
export { buildSegmentRig } from "./rig.js";
|
|
22
|
+
export { resolveLook, LOOK_PRESETS } from "./presets.js";
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { AmbientLight, DirectionalLight, HemisphereLight, Object3D } from "three/webgpu";
|
|
2
|
+
import type { Scene, WebGPURenderer } from "three/webgpu";
|
|
3
|
+
import type { LightingPreset, LookPreset } from "./presets.js";
|
|
4
|
+
export type Vec3Tuple = [number, number, number];
|
|
5
|
+
/**
|
|
6
|
+
* Snap a world point to the shadow map's texel grid in light space, so a
|
|
7
|
+
* shadow camera that follows the player moves in whole texels and the
|
|
8
|
+
* shadow edges never shimmer. `dir` is the light's travel-from direction
|
|
9
|
+
* (unit), `texel` the world size of one shadow texel.
|
|
10
|
+
*/
|
|
11
|
+
export declare function snapToTexel(p: readonly [number, number, number], dir: readonly [number, number, number], texel: number): Vec3Tuple;
|
|
12
|
+
export interface LightingOptions {
|
|
13
|
+
readonly preset?: LookPreset;
|
|
14
|
+
/** Override pieces of the preset. */
|
|
15
|
+
readonly overrides?: Partial<LightingPreset>;
|
|
16
|
+
/** Shadow map size in texels; 0 turns shadows off. Default 2048. */
|
|
17
|
+
readonly shadowMapSize?: number;
|
|
18
|
+
/** Half-size of the square area around the focus that receives shadows, m. Default 26. */
|
|
19
|
+
readonly shadowExtent?: number;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Hemisphere + one directional light whose single shadow map follows a
|
|
23
|
+
* focus point (the player), snapped to texels. Sized for a top-down or
|
|
24
|
+
* third-person view: one tuned map beats cascades at this camera distance.
|
|
25
|
+
*/
|
|
26
|
+
export declare class LightingRig {
|
|
27
|
+
private readonly renderer;
|
|
28
|
+
readonly root: Object3D<import("three").Object3DEventMap>;
|
|
29
|
+
readonly hemi: HemisphereLight;
|
|
30
|
+
readonly sun: DirectionalLight;
|
|
31
|
+
readonly ambient: AmbientLight;
|
|
32
|
+
private dir;
|
|
33
|
+
private extent;
|
|
34
|
+
private mapSize;
|
|
35
|
+
private readonly distance;
|
|
36
|
+
constructor(renderer: WebGPURenderer, scene: Scene, opts?: LightingOptions);
|
|
37
|
+
/** World size of one shadow texel at the current settings. */
|
|
38
|
+
get texel(): number;
|
|
39
|
+
get shadowsOn(): boolean;
|
|
40
|
+
/** Change the shadow resolution (quality tier); 0 turns shadows off. */
|
|
41
|
+
setShadowMapSize(size: number): void;
|
|
42
|
+
/** Half-size of the shadowed square around the focus, m (fit it to what the camera shows). */
|
|
43
|
+
setShadowExtent(extent: number): void;
|
|
44
|
+
private applyExtent;
|
|
45
|
+
/** Move the shadowed area to follow a focus point (texel-snapped). */
|
|
46
|
+
follow(x: number, y: number, z: number): void;
|
|
47
|
+
/** Switch the preset colors and intensities in place. */
|
|
48
|
+
setPreset(preset: LookPreset, overrides?: Partial<LightingPreset>): void;
|
|
49
|
+
dispose(): void;
|
|
50
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { AmbientLight, Color, DirectionalLight, HemisphereLight, Object3D, PCFShadowMap } from "three/webgpu";
|
|
2
|
+
import { resolveLook } from "./presets.js";
|
|
3
|
+
/**
|
|
4
|
+
* Snap a world point to the shadow map's texel grid in light space, so a
|
|
5
|
+
* shadow camera that follows the player moves in whole texels and the
|
|
6
|
+
* shadow edges never shimmer. `dir` is the light's travel-from direction
|
|
7
|
+
* (unit), `texel` the world size of one shadow texel.
|
|
8
|
+
*/
|
|
9
|
+
export function snapToTexel(p, dir, texel) {
|
|
10
|
+
const [dx, dy, dz] = normalize(dir);
|
|
11
|
+
// A right/up basis perpendicular to the light direction.
|
|
12
|
+
let rx = dz;
|
|
13
|
+
let ry = 0;
|
|
14
|
+
let rz = -dx;
|
|
15
|
+
const rl = Math.hypot(rx, ry, rz);
|
|
16
|
+
if (rl < 1e-6) {
|
|
17
|
+
rx = 1;
|
|
18
|
+
ry = 0;
|
|
19
|
+
rz = 0;
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
rx /= rl;
|
|
23
|
+
rz /= rl;
|
|
24
|
+
}
|
|
25
|
+
// up = dir x right
|
|
26
|
+
const ux = dy * rz - dz * ry;
|
|
27
|
+
const uy = dz * rx - dx * rz;
|
|
28
|
+
const uz = dx * ry - dy * rx;
|
|
29
|
+
const a = p[0] * rx + p[1] * ry + p[2] * rz;
|
|
30
|
+
const b = p[0] * ux + p[1] * uy + p[2] * uz;
|
|
31
|
+
const c = p[0] * dx + p[1] * dy + p[2] * dz;
|
|
32
|
+
const sa = texel > 0 ? Math.round(a / texel) * texel : a;
|
|
33
|
+
const sb = texel > 0 ? Math.round(b / texel) * texel : b;
|
|
34
|
+
return [rx * sa + ux * sb + dx * c, ry * sa + uy * sb + dy * c, rz * sa + uz * sb + dz * c];
|
|
35
|
+
}
|
|
36
|
+
function normalize(v) {
|
|
37
|
+
const l = Math.hypot(v[0], v[1], v[2]) || 1;
|
|
38
|
+
return [v[0] / l, v[1] / l, v[2] / l];
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Hemisphere + one directional light whose single shadow map follows a
|
|
42
|
+
* focus point (the player), snapped to texels. Sized for a top-down or
|
|
43
|
+
* third-person view: one tuned map beats cascades at this camera distance.
|
|
44
|
+
*/
|
|
45
|
+
export class LightingRig {
|
|
46
|
+
renderer;
|
|
47
|
+
root = new Object3D();
|
|
48
|
+
hemi;
|
|
49
|
+
sun;
|
|
50
|
+
ambient;
|
|
51
|
+
dir;
|
|
52
|
+
extent;
|
|
53
|
+
mapSize;
|
|
54
|
+
distance = 60;
|
|
55
|
+
constructor(renderer, scene, opts = {}) {
|
|
56
|
+
this.renderer = renderer;
|
|
57
|
+
const p = { ...resolveLook(opts.preset).lighting, ...opts.overrides };
|
|
58
|
+
this.hemi = new HemisphereLight(p.sky, p.ground, p.hemi);
|
|
59
|
+
this.sun = new DirectionalLight(p.sun, p.sunIntensity);
|
|
60
|
+
this.ambient = new AmbientLight(p.ambientColor, p.ambient);
|
|
61
|
+
this.dir = normalize(p.sunDir);
|
|
62
|
+
this.extent = opts.shadowExtent ?? 26;
|
|
63
|
+
this.mapSize = 0;
|
|
64
|
+
this.root.name = "LightingRig";
|
|
65
|
+
this.root.add(this.hemi, this.sun, this.sun.target, this.ambient);
|
|
66
|
+
scene.add(this.root);
|
|
67
|
+
renderer.shadowMap.type = PCFShadowMap;
|
|
68
|
+
this.setShadowMapSize(opts.shadowMapSize ?? 2048);
|
|
69
|
+
this.sun.shadow.intensity = p.shadowStrength;
|
|
70
|
+
this.follow(0, 0, 0);
|
|
71
|
+
}
|
|
72
|
+
/** World size of one shadow texel at the current settings. */
|
|
73
|
+
get texel() {
|
|
74
|
+
return this.mapSize > 0 ? (this.extent * 2) / this.mapSize : 0;
|
|
75
|
+
}
|
|
76
|
+
get shadowsOn() {
|
|
77
|
+
return this.mapSize > 0;
|
|
78
|
+
}
|
|
79
|
+
/** Change the shadow resolution (quality tier); 0 turns shadows off. */
|
|
80
|
+
setShadowMapSize(size) {
|
|
81
|
+
const s = Math.max(0, Math.floor(size));
|
|
82
|
+
if (s === this.mapSize)
|
|
83
|
+
return;
|
|
84
|
+
this.mapSize = s;
|
|
85
|
+
const on = s > 0;
|
|
86
|
+
this.renderer.shadowMap.enabled = on || this.renderer.shadowMap.enabled;
|
|
87
|
+
this.sun.castShadow = on;
|
|
88
|
+
if (!on)
|
|
89
|
+
return;
|
|
90
|
+
const sh = this.sun.shadow;
|
|
91
|
+
sh.mapSize.set(s, s);
|
|
92
|
+
// Bias tuned for a 1-2 texel slope at these extents; normalBias kills acne on curved crowds.
|
|
93
|
+
sh.bias = -0.0005;
|
|
94
|
+
sh.normalBias = 0.03;
|
|
95
|
+
sh.radius = 2.5;
|
|
96
|
+
this.applyExtent();
|
|
97
|
+
// A resized map needs a new render target.
|
|
98
|
+
sh.map?.dispose();
|
|
99
|
+
sh.map = null;
|
|
100
|
+
}
|
|
101
|
+
/** Half-size of the shadowed square around the focus, m (fit it to what the camera shows). */
|
|
102
|
+
setShadowExtent(extent) {
|
|
103
|
+
this.extent = Math.max(2, extent);
|
|
104
|
+
this.applyExtent();
|
|
105
|
+
}
|
|
106
|
+
applyExtent() {
|
|
107
|
+
const cam = this.sun.shadow.camera;
|
|
108
|
+
cam.left = -this.extent;
|
|
109
|
+
cam.right = this.extent;
|
|
110
|
+
cam.top = this.extent;
|
|
111
|
+
cam.bottom = -this.extent;
|
|
112
|
+
cam.near = 1;
|
|
113
|
+
cam.far = this.distance * 2 + 20;
|
|
114
|
+
cam.updateProjectionMatrix();
|
|
115
|
+
}
|
|
116
|
+
/** Move the shadowed area to follow a focus point (texel-snapped). */
|
|
117
|
+
follow(x, y, z) {
|
|
118
|
+
const [sx, sy, sz] = snapToTexel([x, y, z], this.dir, this.texel);
|
|
119
|
+
const d = this.distance;
|
|
120
|
+
this.sun.position.set(sx + this.dir[0] * d, sy + this.dir[1] * d, sz + this.dir[2] * d);
|
|
121
|
+
this.sun.target.position.set(sx, sy, sz);
|
|
122
|
+
this.sun.updateMatrixWorld();
|
|
123
|
+
this.sun.target.updateMatrixWorld();
|
|
124
|
+
}
|
|
125
|
+
/** Switch the preset colors and intensities in place. */
|
|
126
|
+
setPreset(preset, overrides = {}) {
|
|
127
|
+
const p = { ...resolveLook(preset).lighting, ...overrides };
|
|
128
|
+
this.hemi.color = new Color(p.sky);
|
|
129
|
+
this.hemi.groundColor = new Color(p.ground);
|
|
130
|
+
this.hemi.intensity = p.hemi;
|
|
131
|
+
this.sun.color = new Color(p.sun);
|
|
132
|
+
this.sun.intensity = p.sunIntensity;
|
|
133
|
+
this.ambient.color = new Color(p.ambientColor);
|
|
134
|
+
this.ambient.intensity = p.ambient;
|
|
135
|
+
this.sun.shadow.intensity = p.shadowStrength;
|
|
136
|
+
this.dir = normalize(p.sunDir);
|
|
137
|
+
}
|
|
138
|
+
dispose() {
|
|
139
|
+
this.sun.shadow.map?.dispose();
|
|
140
|
+
this.root.removeFromParent();
|
|
141
|
+
}
|
|
142
|
+
}
|