@driftengine/core 4.2.1 → 4.4.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.json +272 -0
- package/README.md +1 -1
- package/dist/cinematic/cameraPath.d.ts +53 -0
- package/dist/cinematic/cameraPath.js +118 -0
- package/dist/environment/daylight.d.ts +77 -0
- package/dist/environment/daylight.js +77 -0
- package/dist/geometry/meshConcat.d.ts +9 -0
- package/dist/geometry/meshConcat.js +101 -0
- package/dist/index.d.ts +12 -2
- package/dist/index.js +14 -1
- package/dist/render/backend/createRenderer.d.ts +13 -1
- package/dist/render/backend/createRenderer.js +6 -5
- package/dist/render/backend/webgl2/renderer.d.ts +166 -1
- package/dist/render/backend/webgl2/renderer.js +424 -23
- package/dist/render/backend/webgpu/buffers.js +46 -8
- package/dist/render/backend/webgpu/depthPass.d.ts +81 -3
- package/dist/render/backend/webgpu/depthPass.js +75 -14
- package/dist/render/backend/webgpu/exposurePass.d.ts +39 -0
- package/dist/render/backend/webgpu/exposurePass.js +190 -0
- package/dist/render/backend/webgpu/fieldCompose.d.ts +11 -0
- package/dist/render/backend/webgpu/fieldCompose.js +13 -0
- package/dist/render/backend/webgpu/flatPass.d.ts +3 -1
- package/dist/render/backend/webgpu/flatPass.js +12 -5
- package/dist/render/backend/webgpu/particlePass.d.ts +8 -0
- package/dist/render/backend/webgpu/particlePass.js +1 -0
- package/dist/render/backend/webgpu/postPass.d.ts +12 -2
- package/dist/render/backend/webgpu/probeBake.d.ts +59 -3
- package/dist/render/backend/webgpu/probeBake.js +97 -7
- package/dist/render/backend/webgpu/probeTargets.d.ts +65 -0
- package/dist/render/backend/webgpu/probeTargets.js +180 -0
- package/dist/render/backend/webgpu/renderer.d.ts +176 -11
- package/dist/render/backend/webgpu/renderer.js +680 -250
- package/dist/render/backend/webgpu/scatterPass.js +7 -7
- package/dist/render/backend/webgpu/shaders/clusterBin.wgsl.d.ts +1 -1
- package/dist/render/backend/webgpu/skinPaletteRing.d.ts +4 -0
- package/dist/render/backend/webgpu/skinPaletteRing.js +9 -0
- package/dist/render/backend/webgpu/skyPass.js +5 -1
- package/dist/render/backend/webgpu/uniformRing.d.ts +31 -1
- package/dist/render/backend/webgpu/uniformRing.js +53 -3
- package/dist/render/bloomChain.d.ts +6 -0
- package/dist/render/bloomChain.js +8 -0
- package/dist/render/clusteredLights.d.ts +19 -11
- package/dist/render/clusteredLights.js +96 -45
- package/dist/render/cutoutCaster.d.ts +23 -0
- package/dist/render/cutoutCaster.js +20 -0
- package/dist/render/driftLight/bake.d.ts +34 -0
- package/dist/render/driftLight/bake.js +95 -0
- package/dist/render/driftLight/falloff.d.ts +16 -0
- package/dist/render/driftLight/falloff.js +23 -0
- package/dist/render/driftLight/layout.d.ts +59 -0
- package/dist/render/driftLight/layout.js +138 -0
- package/dist/render/driftLight/lightField.d.ts +108 -0
- package/dist/render/driftLight/lightField.js +179 -0
- package/dist/render/driftLight/uniforms.d.ts +28 -0
- package/dist/render/driftLight/uniforms.js +41 -0
- package/dist/render/driftLight/visibility.d.ts +18 -0
- package/dist/render/driftLight/visibility.js +47 -0
- package/dist/render/emptyTexture.d.ts +6 -0
- package/dist/render/emptyTexture.js +19 -0
- package/dist/render/exposurePass.d.ts +48 -0
- package/dist/render/exposurePass.js +172 -0
- package/dist/render/flameLight.d.ts +71 -0
- package/dist/render/flameLight.js +63 -0
- package/dist/render/gi/bounceLights.d.ts +34 -0
- package/dist/render/gi/bounceLights.js +66 -0
- package/dist/render/gi/fieldReach.d.ts +12 -0
- package/dist/render/gi/fieldReach.js +62 -0
- package/dist/render/gi/globalField.d.ts +8 -0
- package/dist/render/gi/globalField.js +31 -0
- package/dist/render/gi/probeTrace.d.ts +3 -1
- package/dist/render/gi/probeTrace.js +3 -1
- package/dist/render/gpuCapability.js +19 -4
- package/dist/render/lightBudget.d.ts +11 -0
- package/dist/render/lightBudget.js +11 -0
- package/dist/render/pointLightSelection.d.ts +16 -0
- package/dist/render/pointLightSelection.js +178 -83
- package/dist/render/probeGrid.d.ts +11 -1
- package/dist/render/probeGrid.js +5 -2
- package/dist/render/probeSweeps.d.ts +45 -0
- package/dist/render/probeSweeps.js +77 -0
- package/dist/render/reflectionProbe.d.ts +43 -1
- package/dist/render/reflectionProbe.js +17 -3
- package/dist/render/renderQuality.d.ts +3 -1
- package/dist/render/rendererHarness.d.ts +9 -0
- package/dist/render/rendererHarness.js +18 -3
- package/dist/render/sceneTarget.d.ts +20 -0
- package/dist/render/sceneTarget.js +62 -1
- package/dist/render/shaders/agx.d.ts +31 -0
- package/dist/render/shaders/agx.js +81 -0
- package/dist/render/shaders/ambientOcclusion.d.ts +24 -5
- package/dist/render/shaders/ambientOcclusion.js +120 -63
- package/dist/render/shaders/cutoutCoverage.d.ts +31 -0
- package/dist/render/shaders/cutoutCoverage.js +40 -0
- package/dist/render/shaders/depth.d.ts +25 -0
- package/dist/render/shaders/depth.js +69 -3
- package/dist/render/shaders/exposure.d.ts +52 -0
- package/dist/render/shaders/exposure.js +124 -0
- package/dist/render/shaders/filmLook.d.ts +47 -0
- package/dist/render/shaders/filmLook.js +98 -0
- package/dist/render/shaders/flat/directionalShadow.js +35 -4
- package/dist/render/shaders/flat/driftLight.d.ts +29 -0
- package/dist/render/shaders/flat/driftLight.js +75 -0
- package/dist/render/shaders/flat/index.js +5 -0
- package/dist/render/shaders/flat/lobes.js +13 -0
- package/dist/render/shaders/flat/main.d.ts +1 -1
- package/dist/render/shaders/flat/main.js +68 -9
- package/dist/render/shaders/flat/preamble.js +23 -5
- package/dist/render/shaders/flat/probeGrid.js +46 -3
- package/dist/render/shaders/generated/ambientOcclusion.wgsl.d.ts +1 -1
- package/dist/render/shaders/generated/ambientOcclusion.wgsl.js +1 -1
- package/dist/render/shaders/generated/depth.wgsl.d.ts +100 -0
- package/dist/render/shaders/generated/depth.wgsl.js +100 -0
- package/dist/render/shaders/generated/exposure.wgsl.d.ts +42 -0
- package/dist/render/shaders/generated/exposure.wgsl.js +48 -0
- package/dist/render/shaders/generated/flat.wgsl.d.ts +1800 -1344
- package/dist/render/shaders/generated/flat.wgsl.js +1816 -1360
- package/dist/render/shaders/generated/localExposure.wgsl.d.ts +18 -0
- package/dist/render/shaders/generated/localExposure.wgsl.js +24 -0
- package/dist/render/shaders/generated/rush.wgsl.d.ts +40 -10
- package/dist/render/shaders/generated/rush.wgsl.js +40 -10
- package/dist/render/shaders/generated/temporalResolve.wgsl.d.ts +1 -1
- package/dist/render/shaders/generated/temporalResolve.wgsl.js +1 -1
- package/dist/render/shaders/gi/driftLight.wgsl.d.ts +16 -0
- package/dist/render/shaders/gi/driftLight.wgsl.js +49 -0
- package/dist/render/shaders/gi/probeBake.wgsl.d.ts +29 -4
- package/dist/render/shaders/gi/probeBake.wgsl.js +190 -20
- package/dist/render/shaders/globalMedium.js +4 -0
- package/dist/render/shaders/localExposure.d.ts +35 -0
- package/dist/render/shaders/localExposure.js +138 -0
- package/dist/render/shaders/prefilter.d.ts +1 -1
- package/dist/render/shaders/prefilter.js +6 -0
- package/dist/render/shaders/rush.js +33 -5
- package/dist/render/shaders/tangentFrame.d.ts +1 -1
- package/dist/render/shaders/tangentFrame.js +15 -3
- package/dist/render/shaders/temporalResolve.js +39 -1
- package/dist/render/shadowCasters.d.ts +5 -3
- package/dist/render/shadowMap.js +13 -0
- package/dist/render/surfaceTexture.d.ts +5 -0
- package/package.json +6 -6
- package/scripts/gpuCompute.mjs +24 -3
- package/scripts/png.mjs +23 -12
- package/src/cinematic/cameraPath.ts +178 -0
- package/src/environment/daylight.ts +152 -0
- package/src/geometry/meshConcat.ts +100 -0
- package/src/index.ts +32 -0
- package/src/render/backend/createRenderer.ts +22 -6
- package/src/render/backend/webgl2/renderer.ts +462 -20
- package/src/render/backend/webgpu/buffers.ts +51 -8
- package/src/render/backend/webgpu/depthPass.ts +78 -10
- package/src/render/backend/webgpu/exposurePass.ts +234 -0
- package/src/render/backend/webgpu/fieldCompose.ts +15 -0
- package/src/render/backend/webgpu/flatPass.ts +10 -2
- package/src/render/backend/webgpu/particlePass.ts +9 -0
- package/src/render/backend/webgpu/probeBake.ts +143 -9
- package/src/render/backend/webgpu/probeTargets.ts +211 -0
- package/src/render/backend/webgpu/renderer.ts +807 -271
- package/src/render/backend/webgpu/scatterPass.ts +7 -7
- package/src/render/backend/webgpu/skinPaletteRing.ts +10 -0
- package/src/render/backend/webgpu/skyPass.ts +5 -1
- package/src/render/backend/webgpu/uniformRing.ts +49 -3
- package/src/render/bloomChain.ts +9 -0
- package/src/render/clusteredLights.ts +108 -63
- package/src/render/cutoutCaster.ts +44 -0
- package/src/render/driftLight/bake.ts +125 -0
- package/src/render/driftLight/falloff.ts +26 -0
- package/src/render/driftLight/layout.ts +177 -0
- package/src/render/driftLight/lightField.ts +227 -0
- package/src/render/driftLight/uniforms.ts +61 -0
- package/src/render/driftLight/visibility.ts +58 -0
- package/src/render/emptyTexture.ts +41 -0
- package/src/render/exposurePass.ts +186 -0
- package/src/render/flameLight.ts +135 -0
- package/src/render/gi/bounceLights.ts +68 -0
- package/src/render/gi/fieldReach.ts +74 -0
- package/src/render/gi/globalField.ts +39 -0
- package/src/render/gi/probeTrace.ts +3 -1
- package/src/render/gpuCapability.ts +18 -5
- package/src/render/lightBudget.ts +12 -0
- package/src/render/pointLightSelection.ts +191 -79
- package/src/render/probeGrid.ts +13 -2
- package/src/render/probeSweeps.ts +80 -0
- package/src/render/reflectionProbe.ts +54 -2
- package/src/render/renderQuality.ts +3 -1
- package/src/render/rendererHarness.ts +25 -3
- package/src/render/sceneTarget.ts +75 -1
- package/src/render/shaders/ambientOcclusion.ts +125 -63
- package/src/render/shaders/cutoutCoverage.ts +43 -0
- package/src/render/shaders/depth.ts +80 -3
- package/src/render/shaders/exposure.ts +128 -0
- package/src/render/shaders/filmLook.ts +111 -0
- package/src/render/shaders/flat/directionalShadow.ts +35 -4
- package/src/render/shaders/flat/driftLight.ts +75 -0
- package/src/render/shaders/flat/index.ts +5 -0
- package/src/render/shaders/flat/lobes.ts +13 -0
- package/src/render/shaders/flat/main.ts +68 -9
- package/src/render/shaders/flat/preamble.ts +23 -5
- package/src/render/shaders/flat/probeGrid.ts +46 -3
- package/src/render/shaders/generated/ambientOcclusion.wgsl.ts +1 -1
- package/src/render/shaders/generated/depth.wgsl.ts +103 -0
- package/src/render/shaders/generated/exposure.wgsl.ts +51 -0
- package/src/render/shaders/generated/flat.wgsl.ts +1816 -1360
- package/src/render/shaders/generated/localExposure.wgsl.ts +26 -0
- package/src/render/shaders/generated/rush.wgsl.ts +40 -10
- package/src/render/shaders/generated/temporalResolve.wgsl.ts +1 -1
- package/src/render/shaders/gi/driftLight.wgsl.ts +49 -0
- package/src/render/shaders/gi/probeBake.wgsl.ts +191 -20
- package/src/render/shaders/globalMedium.ts +4 -0
- package/src/render/shaders/localExposure.ts +154 -0
- package/src/render/shaders/prefilter.ts +6 -0
- package/src/render/shaders/rush.ts +33 -5
- package/src/render/shaders/tangentFrame.ts +15 -3
- package/src/render/shaders/temporalResolve.ts +39 -1
- package/src/render/shadowCasters.ts +5 -3
- package/src/render/shadowMap.ts +14 -0
- package/src/render/surfaceTexture.ts +5 -0
|
@@ -37,6 +37,12 @@ import { OCTAHEDRAL_GLSL } from './octahedral.js';
|
|
|
37
37
|
*/
|
|
38
38
|
export const PREFILTER_FRAG = `#version 300 es
|
|
39
39
|
precision highp float;
|
|
40
|
+
/*
|
|
41
|
+
* highp int as well, because the radical inverse below is a 32-bit bit reversal and the fragment
|
|
42
|
+
* default for int and uint is mediump. A driver that computes mediump in sixteen bits returns 0
|
|
43
|
+
* for every sample, and every cosine sample of the diffuse level lands on its own normal.
|
|
44
|
+
*/
|
|
45
|
+
precision highp int;
|
|
40
46
|
|
|
41
47
|
${OCTAHEDRAL_GLSL}
|
|
42
48
|
|
|
@@ -21,8 +21,16 @@
|
|
|
21
21
|
/* The vertex stage is `FULLSCREEN_VERT`, shared with the occlusion pass and its blur. */
|
|
22
22
|
import { glslSceneDepthToNdc } from '../depthConvention.js';
|
|
23
23
|
import { DEPTH_OF_FIELD_GLSL } from './depthOfField.js';
|
|
24
|
+
import { AUTO_EXPOSURE_GLSL } from './exposure.js';
|
|
25
|
+
import { LOCAL_EXPOSURE_GLSL } from './localExposure.js';
|
|
26
|
+
import { FILM_LOOK_GLSL } from './filmLook.js';
|
|
24
27
|
export const RUSH_FRAG = `#version 300 es
|
|
25
28
|
precision highp float;
|
|
29
|
+
/*
|
|
30
|
+
* highp int as well, for the film grain's hash, which multiplies and shifts uints. The fragment
|
|
31
|
+
* default is mediump, and a sixteen-bit driver would hand back a hash of sixteen bits.
|
|
32
|
+
*/
|
|
33
|
+
precision highp int;
|
|
26
34
|
|
|
27
35
|
in vec2 vUv;
|
|
28
36
|
|
|
@@ -133,6 +141,11 @@ uniform float uBloomStrength;
|
|
|
133
141
|
*/
|
|
134
142
|
uniform vec3 uVeilColor;
|
|
135
143
|
uniform float uVeilAlpha;
|
|
144
|
+
${FILM_LOOK_GLSL}
|
|
145
|
+
|
|
146
|
+
/* Eye adaptation's held brightness and the gain it asks for. See exposure.ts. */
|
|
147
|
+
${AUTO_EXPOSURE_GLSL}
|
|
148
|
+
${LOCAL_EXPOSURE_GLSL}
|
|
136
149
|
|
|
137
150
|
/**
|
|
138
151
|
* The colour grade: a lookup table over display values, sampled after the tone map.
|
|
@@ -291,6 +304,24 @@ vec3 cameraBlur(vec3 scene) {
|
|
|
291
304
|
return sum / 9.0;
|
|
292
305
|
}
|
|
293
306
|
|
|
307
|
+
/**
|
|
308
|
+
* Everything after the light is complete, in the order the header of each step argues: the lens
|
|
309
|
+
* darkens its corners in scene light, the curve and the grade turn light into a picture, the veil
|
|
310
|
+
* fades it, and the print's grain sits on what is left. One function, so the two exits of \`main\`
|
|
311
|
+
* cannot finish a frame differently.
|
|
312
|
+
*/
|
|
313
|
+
vec3 finish(vec3 light) {
|
|
314
|
+
/*
|
|
315
|
+
* The eye's adaptation, in scene light and before the lens, as a camera's exposure would be; and
|
|
316
|
+
* each region's share of it, read from the light before either scales it, because that is the
|
|
317
|
+
* light the grid measured.
|
|
318
|
+
*/
|
|
319
|
+
float local = localExposureGain(vUv, light);
|
|
320
|
+
light *= autoExposureGain() * local;
|
|
321
|
+
vec3 lensed = withVignette(light, vUv, vec2(textureSize(uScene, 0)));
|
|
322
|
+
return withGrain(withVeil(applyColourGrade(grade(lensed))), gl_FragCoord.xy);
|
|
323
|
+
}
|
|
324
|
+
|
|
294
325
|
void main() {
|
|
295
326
|
vec3 scene = textureLod(uScene, vUv, 0.0).rgb;
|
|
296
327
|
if (uMotionStrength > 0.0) scene = cameraBlur(scene);
|
|
@@ -328,7 +359,7 @@ void main() {
|
|
|
328
359
|
float edge = smoothstep(inner, 1.0, radius);
|
|
329
360
|
float amount = edge * uStrength;
|
|
330
361
|
if (amount <= 0.0) {
|
|
331
|
-
fragColor = vec4(
|
|
362
|
+
fragColor = vec4(finish(withBloom(scene * ao)), 1.0);
|
|
332
363
|
return;
|
|
333
364
|
}
|
|
334
365
|
|
|
@@ -347,9 +378,6 @@ void main() {
|
|
|
347
378
|
}
|
|
348
379
|
vec3 blurred = sum / 7.0;
|
|
349
380
|
|
|
350
|
-
fragColor = vec4(
|
|
351
|
-
withVeil(applyColourGrade(grade(withBloom(mix(scene, blurred, amount) * ao)))),
|
|
352
|
-
1.0
|
|
353
|
-
);
|
|
381
|
+
fragColor = vec4(finish(withBloom(mix(scene, blurred, amount) * ao)), 1.0);
|
|
354
382
|
}
|
|
355
383
|
`;
|
|
@@ -14,4 +14,4 @@
|
|
|
14
14
|
* reason beside it; `AGENTS.md` 2026-08-07 is the rule, and WGSL refuses the module outright
|
|
15
15
|
* rather than leaving a derivative in non-uniform control flow undefined.
|
|
16
16
|
*/
|
|
17
|
-
export declare const TANGENT_FRAME_GLSL = "mat3 tangentFrame(vec3 n, vec3 worldPos, vec2 uv, vec4 tangent, int hasTangents) {\n /*\n * The derived frame: solve for the axis along u from how position and uv change together over\n * the pixel. Exact where the surface is locally planar, approximate where it curves, which is\n * the trade for working at all on geometry that carries no frame.\n */\n vec3 dp1 = dFdx(worldPos);\n vec3 dp2 = dFdy(worldPos);\n vec2 duv1 = dFdx(uv);\n vec2 duv2 = dFdy(uv);\n vec3 dp2perp = cross(dp2, n);\n vec3 dp1perp = cross(n, dp1);\n vec3 dt = dp2perp * duv1.x + dp1perp * duv2.x;\n vec3 db = dp2perp * duv1.y + dp1perp * duv2.y;\n /*\n * **The determinant's sign, which magnitude-normalising throws away \u2014 and it is the difference\n * between the two backends.**\n *\n * dFdy measures along *upward* window y in GLSL and along *downward* framebuffer y in WGSL, so\n * dp2 and duv2 both arrive negated on WebGPU. The system this solves is invariant under that \u2014\n * both sides of its second equation flip \u2014 but the closed form above is not: dt and db each come\n * out negated, and dividing by their *length* keeps the negation instead of cancelling it. The\n * frame then points the other way on one backend, which reads as a surface lit from the wrong\n * side and nothing else.\n *\n * The UV determinant flips with them, so folding its sign back in restores the invariance the\n * solve always had. Measured before and after on demo/dev/normal.html: the derived panel stood\n * 86 of 255 rms from the attribute panel on WebGPU and 8.8 on WebGL2; with this they agree.\n */\n float det = duv1.x * duv2.y - duv2.x * duv1.y;\n float handed = det < 0.0 ? -1.0 : 1.0;\n /* One scale for both axes, so a stretched UV layout does not shear the frame it produces. */\n float invmax = inversesqrt(max(max(dot(dt, dt), dot(db, db)), 1e-12)) * handed;\n vec3 derivedT = dt * invmax;\n vec3 derivedB = db * invmax;\n\n /*\n * The attribute frame, re-orthogonalised. Interpolating a tangent across a triangle leaves it\n * no longer square to the interpolated normal, and Gram-Schmidt is what puts it back \u2014\n * generateTangents does the same per vertex and says why: dropping it is a normal map that\n * shears wherever the geometry curves.\n */\n vec3
|
|
17
|
+
export declare const TANGENT_FRAME_GLSL = "mat3 tangentFrame(vec3 n, vec3 worldPos, vec2 uv, vec4 tangent, int hasTangents) {\n /*\n * The derived frame: solve for the axis along u from how position and uv change together over\n * the pixel. Exact where the surface is locally planar, approximate where it curves, which is\n * the trade for working at all on geometry that carries no frame.\n */\n vec3 dp1 = dFdx(worldPos);\n vec3 dp2 = dFdy(worldPos);\n vec2 duv1 = dFdx(uv);\n vec2 duv2 = dFdy(uv);\n vec3 dp2perp = cross(dp2, n);\n vec3 dp1perp = cross(n, dp1);\n vec3 dt = dp2perp * duv1.x + dp1perp * duv2.x;\n vec3 db = dp2perp * duv1.y + dp1perp * duv2.y;\n /*\n * **The determinant's sign, which magnitude-normalising throws away \u2014 and it is the difference\n * between the two backends.**\n *\n * dFdy measures along *upward* window y in GLSL and along *downward* framebuffer y in WGSL, so\n * dp2 and duv2 both arrive negated on WebGPU. The system this solves is invariant under that \u2014\n * both sides of its second equation flip \u2014 but the closed form above is not: dt and db each come\n * out negated, and dividing by their *length* keeps the negation instead of cancelling it. The\n * frame then points the other way on one backend, which reads as a surface lit from the wrong\n * side and nothing else.\n *\n * The UV determinant flips with them, so folding its sign back in restores the invariance the\n * solve always had. Measured before and after on demo/dev/normal.html: the derived panel stood\n * 86 of 255 rms from the attribute panel on WebGPU and 8.8 on WebGL2; with this they agree.\n */\n float det = duv1.x * duv2.y - duv2.x * duv1.y;\n float handed = det < 0.0 ? -1.0 : 1.0;\n /* One scale for both axes, so a stretched UV layout does not shear the frame it produces. */\n float invmax = inversesqrt(max(max(dot(dt, dt), dot(db, db)), 1e-12)) * handed;\n vec3 derivedT = dt * invmax;\n vec3 derivedB = db * invmax;\n\n /*\n * The attribute frame, re-orthogonalised. Interpolating a tangent across a triangle leaves it\n * no longer square to the interpolated normal, and Gram-Schmidt is what puts it back \u2014\n * generateTangents does the same per vertex and says why: dropping it is a normal map that\n * shears wherever the geometry curves.\n */\n vec3 along = tangent.xyz - n * dot(n, tangent.xyz);\n float alongSq = dot(along, along);\n vec3 attrT = along * inversesqrt(max(alongSq, 1e-12));\n /*\n * The bitangent's sign, which is the whole reason w is stored. An artist mapping the left and\n * right of a model onto one patch of texture gives one side a mirrored frame, and a bitangent\n * computed without the sign lights that side inside out.\n */\n vec3 attrB = cross(n, attrT) * tangent.w;\n\n /*\n * **And only where there is a frame to keep.** A tangent parallel to its normal, or two that\n * nearly cancel across one triangle at a mirrored seam, leaves nothing after the subtraction,\n * and a normalised nothing is NaN. One such pixel in one probe face was a whole courtyard drawn\n * black: a probe convolves its faces into every direction, and the bounce carries that into\n * every other probe. So where nothing is left, the derived frame stands in. What it gives up is a\n * direction on a pixel that had none; what would make it wrong is a threshold large enough to\n * catch a frame that is merely short, and a unit tangent interpolated is nowhere near 1e-4.\n */\n bool framed = hasTangents != 0 && alongSq > 1e-8;\n vec3 t = framed ? attrT : derivedT;\n vec3 b = framed ? attrB : derivedB;\n return mat3(t, b, n);\n}";
|
|
@@ -56,7 +56,9 @@ export const TANGENT_FRAME_GLSL = `mat3 tangentFrame(vec3 n, vec3 worldPos, vec2
|
|
|
56
56
|
* generateTangents does the same per vertex and says why: dropping it is a normal map that
|
|
57
57
|
* shears wherever the geometry curves.
|
|
58
58
|
*/
|
|
59
|
-
vec3
|
|
59
|
+
vec3 along = tangent.xyz - n * dot(n, tangent.xyz);
|
|
60
|
+
float alongSq = dot(along, along);
|
|
61
|
+
vec3 attrT = along * inversesqrt(max(alongSq, 1e-12));
|
|
60
62
|
/*
|
|
61
63
|
* The bitangent's sign, which is the whole reason w is stored. An artist mapping the left and
|
|
62
64
|
* right of a model onto one patch of texture gives one side a mirrored frame, and a bitangent
|
|
@@ -64,7 +66,17 @@ export const TANGENT_FRAME_GLSL = `mat3 tangentFrame(vec3 n, vec3 worldPos, vec2
|
|
|
64
66
|
*/
|
|
65
67
|
vec3 attrB = cross(n, attrT) * tangent.w;
|
|
66
68
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
+
/*
|
|
70
|
+
* **And only where there is a frame to keep.** A tangent parallel to its normal, or two that
|
|
71
|
+
* nearly cancel across one triangle at a mirrored seam, leaves nothing after the subtraction,
|
|
72
|
+
* and a normalised nothing is NaN. One such pixel in one probe face was a whole courtyard drawn
|
|
73
|
+
* black: a probe convolves its faces into every direction, and the bounce carries that into
|
|
74
|
+
* every other probe. So where nothing is left, the derived frame stands in. What it gives up is a
|
|
75
|
+
* direction on a pixel that had none; what would make it wrong is a threshold large enough to
|
|
76
|
+
* catch a frame that is merely short, and a unit tangent interpolated is nowhere near 1e-4.
|
|
77
|
+
*/
|
|
78
|
+
bool framed = hasTangents != 0 && alongSq > 1e-8;
|
|
79
|
+
vec3 t = framed ? attrT : derivedT;
|
|
80
|
+
vec3 b = framed ? attrB : derivedB;
|
|
69
81
|
return mat3(t, b, n);
|
|
70
82
|
}`;
|
|
@@ -110,6 +110,44 @@ vec3 clipToNeighbourhood(vec3 history, vec3 lo, vec3 hi) {
|
|
|
110
110
|
return centre + offset / ratio;
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
+
/*
|
|
114
|
+
* **The history read through a Catmull-Rom filter, not a bilinear one.**
|
|
115
|
+
*
|
|
116
|
+
* The reprojection lands between texels nearly everywhere, and a bilinear fetch there is a small blur;
|
|
117
|
+
* applied to a picture that is itself last frame's blend, it compounds every frame, so a still camera
|
|
118
|
+
* settled on a picture measurably softer than one frame of it. A courtyard's stone read as out of
|
|
119
|
+
* focus beside the same frame with the resolve off. Catmull-Rom passes a straight ramp through exactly
|
|
120
|
+
* and keeps the edge between two texels an edge, which is what the history is for.
|
|
121
|
+
*
|
|
122
|
+
* Five bilinear fetches rather than sixteen point ones: the middle two weights of each axis share a
|
|
123
|
+
* fetch placed between them, and the four corners, which carry a few hundredths, are left out and
|
|
124
|
+
* the rest renormalised. Its negative lobes can ring past what the frame holds, which the
|
|
125
|
+
* neighbourhood clip after it takes back, and the floor at zero keeps a float target out of the
|
|
126
|
+
* negatives meanwhile.
|
|
127
|
+
*/
|
|
128
|
+
vec3 historyCatmullRom(vec2 uv) {
|
|
129
|
+
vec2 size = 1.0 / uTexel;
|
|
130
|
+
vec2 at = uv * size;
|
|
131
|
+
vec2 base = floor(at - 0.5) + 0.5;
|
|
132
|
+
vec2 f = at - base;
|
|
133
|
+
vec2 w0 = f * (-0.5 + f * (1.0 - 0.5 * f));
|
|
134
|
+
vec2 w1 = 1.0 + f * f * (-2.5 + 1.5 * f);
|
|
135
|
+
vec2 w2 = f * (0.5 + f * (2.0 - 1.5 * f));
|
|
136
|
+
vec2 w3 = f * f * (-0.5 + 0.5 * f);
|
|
137
|
+
vec2 w12 = w1 + w2;
|
|
138
|
+
vec2 at0 = (base - 1.0) * uTexel;
|
|
139
|
+
vec2 at3 = (base + 2.0) * uTexel;
|
|
140
|
+
vec2 at12 = (base + w2 / w12) * uTexel;
|
|
141
|
+
vec3 sum =
|
|
142
|
+
textureLod(uHistory, vec2(at12.x, at0.y), 0.0).rgb * (w12.x * w0.y) +
|
|
143
|
+
textureLod(uHistory, vec2(at0.x, at12.y), 0.0).rgb * (w0.x * w12.y) +
|
|
144
|
+
textureLod(uHistory, at12, 0.0).rgb * (w12.x * w12.y) +
|
|
145
|
+
textureLod(uHistory, vec2(at3.x, at12.y), 0.0).rgb * (w3.x * w12.y) +
|
|
146
|
+
textureLod(uHistory, vec2(at12.x, at3.y), 0.0).rgb * (w12.x * w3.y);
|
|
147
|
+
float weight = w12.x * w0.y + w0.x * w12.y + w12.x * w12.y + w3.x * w12.y + w12.x * w3.y;
|
|
148
|
+
return max(sum / weight, vec3(0.0));
|
|
149
|
+
}
|
|
150
|
+
|
|
113
151
|
void main() {
|
|
114
152
|
vec3 current = textureLod(uScene, vUv, 0.0).rgb;
|
|
115
153
|
|
|
@@ -142,7 +180,7 @@ void main() {
|
|
|
142
180
|
vec3 lo;
|
|
143
181
|
vec3 hi;
|
|
144
182
|
neighbourhood(lo, hi);
|
|
145
|
-
vec3 history =
|
|
183
|
+
vec3 history = historyCatmullRom(wasUv);
|
|
146
184
|
vec3 bounded = clipToNeighbourhood(history, lo, hi);
|
|
147
185
|
|
|
148
186
|
fragColor = vec4(mix(current, bounded, uHistoryBlend), 1.0);
|
|
@@ -26,9 +26,11 @@ import type { SurfaceTextureHandle } from './backend/api.ts';
|
|
|
26
26
|
* enumeration serves the directional cascade and every point-light cubemap.
|
|
27
27
|
*
|
|
28
28
|
* **Every draw may also carry its material, which is what lets one enumeration serve a colour
|
|
29
|
-
* pass as well as a depth one.** A depth pass
|
|
30
|
-
*
|
|
31
|
-
*
|
|
29
|
+
* pass as well as a depth one.** A depth pass reads one thing from it, since 4.4.0: whether the
|
|
30
|
+
* caster is a cutout (`cutoutOf`), so a leaf card casts the leaf rather than the card. Everything
|
|
31
|
+
* else a material says cannot change a depth, which is why this interface carried only a handle, a
|
|
32
|
+
* matrix and a palette for as long as depth was the only thing replaying it — and why, until then,
|
|
33
|
+
* every alpha-cut surface cast its whole quad. What that cost was measured from outside. A
|
|
32
34
|
* consumer wanting the scene from a second viewpoint — a mirror, a probe face — had one option,
|
|
33
35
|
* which was to run its whole draw path again; the first attempt to avoid it replayed this sink
|
|
34
36
|
* into the mirror and got **every car unpainted**, because the list carries no material. So the
|
package/dist/render/shadowMap.js
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* "zero textures" rule is about the art direction (no image assets in the
|
|
10
10
|
* payload), and nothing here adds a byte to it.
|
|
11
11
|
*/
|
|
12
|
+
import { SHADOW_DEPTH_CLEAR } from './depthConvention.js';
|
|
12
13
|
import { DEFAULT_RENDER_QUALITY } from './renderQuality.js';
|
|
13
14
|
export class ShadowMap {
|
|
14
15
|
size;
|
|
@@ -48,6 +49,18 @@ export class ShadowMap {
|
|
|
48
49
|
return;
|
|
49
50
|
throw new Error(`ShadowMap: framebuffer incomplete (0x${status.toString(16)})`);
|
|
50
51
|
}
|
|
52
|
+
/*
|
|
53
|
+
* **Cleared once, here, to the far plane, because nothing else says what the texture holds.**
|
|
54
|
+
* WebGL zeroes it, and a zero compared with `LEQUAL` is an occluder at the light itself, so
|
|
55
|
+
* a layer no pass has drawn put the whole world in shade. ANGLE on Vulkan hands the same
|
|
56
|
+
* texture back reading 1, which is lit, and that is what every capture here ran on: a scene
|
|
57
|
+
* with a second depth layer it never peeled lost its sun only under ANGLE's GL backend, the
|
|
58
|
+
* one Chrome on Linux runs by default. The frame's clear value is borrowed and put back.
|
|
59
|
+
*/
|
|
60
|
+
const frameClear = gl.getParameter(gl.DEPTH_CLEAR_VALUE);
|
|
61
|
+
gl.clearDepth(SHADOW_DEPTH_CLEAR);
|
|
62
|
+
gl.clear(gl.DEPTH_BUFFER_BIT);
|
|
63
|
+
gl.clearDepth(frameClear);
|
|
51
64
|
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
|
52
65
|
gl.bindTexture(gl.TEXTURE_2D, null);
|
|
53
66
|
}
|
|
@@ -59,6 +59,11 @@ export interface SurfaceMaterial<Texture = SurfaceTexture> {
|
|
|
59
59
|
vScale?: number;
|
|
60
60
|
/** Alpha below which a fragment is discarded. See `uAlbedoCutout`. */
|
|
61
61
|
cutout?: number;
|
|
62
|
+
/**
|
|
63
|
+
* Whether both faces are seen: glTF's `doubleSided`, what a curtain or a leaf card is. Drawn
|
|
64
|
+
* without culling, and a back face is lit as its front, with the normal turned to the viewer.
|
|
65
|
+
*/
|
|
66
|
+
doubleSided?: boolean;
|
|
62
67
|
/**
|
|
63
68
|
* How hard the normal map turns the shading normal.
|
|
64
69
|
*
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@driftengine/core",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.4.0",
|
|
4
4
|
"description": "DriftEngine reusable browser 3D runtime",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -29,8 +29,8 @@
|
|
|
29
29
|
],
|
|
30
30
|
"sideEffects": false,
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"@driftengine/drft": "4.
|
|
33
|
-
"@driftengine/physics": "4.
|
|
32
|
+
"@driftengine/drft": "4.4.0",
|
|
33
|
+
"@driftengine/physics": "4.4.0",
|
|
34
34
|
"gl-matrix": "^3.4.3"
|
|
35
35
|
},
|
|
36
36
|
"author": "Drift Technologies",
|
|
@@ -63,8 +63,8 @@
|
|
|
63
63
|
"prepack": "node ../../scripts/build.mjs @driftengine/core"
|
|
64
64
|
},
|
|
65
65
|
"devDependencies": {
|
|
66
|
-
"@driftengine/nav": "4.
|
|
67
|
-
"@driftengine/network": "4.
|
|
68
|
-
"@driftengine/texture": "4.
|
|
66
|
+
"@driftengine/nav": "4.4.0",
|
|
67
|
+
"@driftengine/network": "4.4.0",
|
|
68
|
+
"@driftengine/texture": "4.4.0"
|
|
69
69
|
}
|
|
70
70
|
}
|
package/scripts/gpuCompute.mjs
CHANGED
|
@@ -125,6 +125,22 @@ function dispatchScript(request) {
|
|
|
125
125
|
});
|
|
126
126
|
continue;
|
|
127
127
|
}
|
|
128
|
+
if (spec.kind === 'emptyVolume') {
|
|
129
|
+
/* A new texture is zeros by the specification, so there is nothing to write. */
|
|
130
|
+
const texture = device.createTexture({
|
|
131
|
+
size: [1, 1, 1],
|
|
132
|
+
dimension: '3d',
|
|
133
|
+
format: spec.format,
|
|
134
|
+
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST,
|
|
135
|
+
});
|
|
136
|
+
entries.push({ binding: at, resource: texture.createView({ dimension: '3d' }) });
|
|
137
|
+
bindings.push({
|
|
138
|
+
binding: at,
|
|
139
|
+
visibility: GPUShaderStage.COMPUTE,
|
|
140
|
+
texture: { sampleType: spec.format === 'r32uint' ? 'uint' : 'float', viewDimension: '3d' },
|
|
141
|
+
});
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
128
144
|
if (spec.kind === 'sampler') {
|
|
129
145
|
const sampler = device.createSampler({
|
|
130
146
|
magFilter: 'linear',
|
|
@@ -225,7 +241,7 @@ export async function openGpuCompute(options = {}) {
|
|
|
225
241
|
* `features` are the device features the shader needs, `'shader-f16'` for a half-precision one;
|
|
226
242
|
* an adapter without one of them is an error rather than a device created without it.
|
|
227
243
|
*
|
|
228
|
-
* Each entry of `buffers` is bound at the binding its index names, as one of
|
|
244
|
+
* Each entry of `buffers` is bound at the binding its index names, as one of six kinds:
|
|
229
245
|
*
|
|
230
246
|
* - `'storage'`, the default — `type`, `values` or `length`, `read`, `readOnly`. An `'f16'`
|
|
231
247
|
* buffer takes half-precision bits as its values, and is never read back.
|
|
@@ -242,14 +258,19 @@ export async function openGpuCompute(options = {}) {
|
|
|
242
258
|
* the device's sampler rounds to.
|
|
243
259
|
* - `'sampler'` — `{ kind: 'sampler', address: 'clamp-to-edge' | 'repeat' }`, linear in
|
|
244
260
|
* magnification, minification and between levels.
|
|
261
|
+
* - `'emptyVolume'` — `{ kind: 'emptyVolume', format: 'r32uint' | 'rgba16float' }`: one texel
|
|
262
|
+
* of zeros in a 3D texture, for a module that declares a volume the check leaves switched off
|
|
263
|
+
* (the probe bake's DriftLight field).
|
|
245
264
|
*
|
|
246
265
|
* @param {{ wgsl: string, entryPoint?: string, workgroups?: number[], features?: string[],
|
|
247
|
-
* buffers: { kind?: 'storage'|'uniform'|'texture2dArray'|'texture2d'|'sampler'
|
|
266
|
+
* buffers: { kind?: 'storage'|'uniform'|'texture2dArray'|'texture2d'|'sampler'|
|
|
267
|
+
* 'emptyVolume',
|
|
248
268
|
* type?: 'f32'|'f16'|'u32'|'i32', values?: number[], length?: number,
|
|
249
269
|
* read?: boolean, readOnly?: boolean, size?: number, layers?: number,
|
|
250
270
|
* levels?: ArrayLike<number>[], address?: string,
|
|
251
271
|
* width?: number, height?: number,
|
|
252
|
-
* format?: 'r32float'|'rgba32float' }[] }}
|
|
272
|
+
* format?: 'r32float'|'rgba32float'|'r32uint'|'rgba16float' }[] }}
|
|
273
|
+
* request
|
|
253
274
|
*/
|
|
254
275
|
async run(request) {
|
|
255
276
|
const prepared = {
|
package/scripts/png.mjs
CHANGED
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
* Enough of a PNG reader to compare two frames, with no dependency.
|
|
3
3
|
*
|
|
4
4
|
* A screenshot arrives as 8-bit RGBA, filter method 0, not interlaced, which is what every
|
|
5
|
-
* browser produces
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* browser produces. A bought model's maps also arrive as 8-bit grey, grey with alpha and RGB, and
|
|
6
|
+
* the baker and the native host read those through here too. Anything else — a palette, sixteen
|
|
7
|
+
* bits, interlacing — is refused by name rather than guessed at, because a reader that quietly
|
|
8
|
+
* mis-decodes produces pixel differences that look like a rendering change.
|
|
8
9
|
*
|
|
9
10
|
* `zlib` is Node's own, so this costs nothing to have.
|
|
10
11
|
*/
|
|
@@ -35,8 +36,15 @@ export function decodePng(bytes) {
|
|
|
35
36
|
const interlace = body[12];
|
|
36
37
|
if (depth !== 8) throw new Error(`bit depth ${depth}, only 8 is handled`);
|
|
37
38
|
if (interlace !== 0) throw new Error('interlaced PNG, not handled');
|
|
38
|
-
|
|
39
|
-
|
|
39
|
+
/*
|
|
40
|
+
* Grey, grey with alpha, RGB and RGBA — every 8-bit type but a palette. A bought model's
|
|
41
|
+
* single-channel maps are greyscale, and refusing them left a baker's texture cap unable to
|
|
42
|
+
* shrink a third of one scene's maps. A palette needs `PLTE` read, and nothing has asked.
|
|
43
|
+
*/
|
|
44
|
+
channels = { 0: 1, 4: 2, 2: 3, 6: 4 }[colorType] ?? 0;
|
|
45
|
+
if (channels === 0) {
|
|
46
|
+
throw new Error(`colour type ${colorType}, only 0, 2, 4 and 6 are handled`);
|
|
47
|
+
}
|
|
40
48
|
} else if (type === 'IDAT') {
|
|
41
49
|
parts.push(body);
|
|
42
50
|
} else if (type === 'IEND') {
|
|
@@ -200,17 +208,20 @@ function chunk(type, body) {
|
|
|
200
208
|
return out;
|
|
201
209
|
}
|
|
202
210
|
|
|
203
|
-
/** A decoded PNG as four bytes a pixel,
|
|
211
|
+
/** A decoded PNG as four bytes a pixel, whatever channels it was written with. The one widening. */
|
|
204
212
|
export function rgbaOf(decoded) {
|
|
205
213
|
const count = decoded.width * decoded.height;
|
|
206
|
-
|
|
207
|
-
|
|
214
|
+
const channels = decoded.channels;
|
|
215
|
+
if (channels === 4) return { width: decoded.width, height: decoded.height, rgba: decoded.pixels };
|
|
208
216
|
const rgba = new Uint8Array(count * 4);
|
|
209
217
|
for (let at = 0; at < count; at += 1) {
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
rgba[at * 4
|
|
218
|
+
const from = at * channels;
|
|
219
|
+
/* Grey fills all three colour channels; RGB keeps its own. */
|
|
220
|
+
const grey = channels < 3;
|
|
221
|
+
rgba[at * 4] = decoded.pixels[from];
|
|
222
|
+
rgba[at * 4 + 1] = decoded.pixels[grey ? from : from + 1];
|
|
223
|
+
rgba[at * 4 + 2] = decoded.pixels[grey ? from : from + 2];
|
|
224
|
+
rgba[at * 4 + 3] = channels === 2 ? decoded.pixels[from + 1] : 255;
|
|
214
225
|
}
|
|
215
226
|
return { width: decoded.width, height: decoded.height, rgba };
|
|
216
227
|
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A camera move through world space: an eye, a point it looks at and a field of view, keyed in time.
|
|
3
|
+
*
|
|
4
|
+
* **Not the cinematic player's shots, which frame a subject.** A shot there is placed relative to
|
|
5
|
+
* something that moves, and its keyframes are linear on purpose because a rig damps them. A walk
|
|
6
|
+
* through a building has no subject: it is a route and a gaze, both in world coordinates, and it
|
|
7
|
+
* has to arrive at each key without a kink or the motion reads as a hand-held stumble.
|
|
8
|
+
*
|
|
9
|
+
* **A cubic Hermite through every key, its tangent the key's neighbours' difference over their
|
|
10
|
+
* time apart**, which is Catmull-Rom with the keys' own times as knots. A key is passed exactly, the
|
|
11
|
+
* velocity through it is continuous, and an evenly timed straight run is exactly a constant
|
|
12
|
+
* velocity. What it gives up is continuity of acceleration — the curvature can jump at a key — and
|
|
13
|
+
* what would make that wrong is a move slow and wide enough for the jump to be seen as a change of
|
|
14
|
+
* pull. The answer then is keys further apart, not another curve.
|
|
15
|
+
*
|
|
16
|
+
* The eye and the target are interpolated separately, so a pan and a dolly are written as what they
|
|
17
|
+
* are. Allocation-free after construction: `sampleCameraPath` fills a caller-owned sample.
|
|
18
|
+
*/
|
|
19
|
+
import type { Vec3 } from '../math/color.ts';
|
|
20
|
+
|
|
21
|
+
export interface CameraPathKey {
|
|
22
|
+
readonly atSec: number;
|
|
23
|
+
readonly eye: Readonly<Vec3>;
|
|
24
|
+
readonly target: Readonly<Vec3>;
|
|
25
|
+
readonly fovDeg: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface CameraPathOptions {
|
|
29
|
+
/**
|
|
30
|
+
* Whether the move wraps back through its first key. A looped path needs `periodSec`: when the
|
|
31
|
+
* first key comes round again, which must be later than the last key.
|
|
32
|
+
*/
|
|
33
|
+
readonly loop: boolean;
|
|
34
|
+
readonly periodSec?: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Seven channels a key: eye, target, field of view. */
|
|
38
|
+
const CHANNELS = 7;
|
|
39
|
+
|
|
40
|
+
export interface CameraPath {
|
|
41
|
+
readonly loop: boolean;
|
|
42
|
+
/** For a loop, the period; for an open path, the time of the last key. */
|
|
43
|
+
readonly durationSec: number;
|
|
44
|
+
readonly count: number;
|
|
45
|
+
readonly times: Float64Array;
|
|
46
|
+
readonly values: Float64Array;
|
|
47
|
+
/** Per key and channel, units a second. */
|
|
48
|
+
readonly tangents: Float64Array;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface CameraPathSample {
|
|
52
|
+
eye: Vec3;
|
|
53
|
+
target: Vec3;
|
|
54
|
+
fovDeg: number;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function createCameraPathSample(): CameraPathSample {
|
|
58
|
+
return { eye: [0, 0, 0], target: [0, 0, 0], fovDeg: 50 };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Check the keys and work out every tangent once. Throws at construction, never in a frame. */
|
|
62
|
+
export function createCameraPath(
|
|
63
|
+
keys: readonly CameraPathKey[],
|
|
64
|
+
options: CameraPathOptions,
|
|
65
|
+
): CameraPath {
|
|
66
|
+
const count = keys.length;
|
|
67
|
+
if (count < 2) throw new Error('a camera path needs at least two keys to move between');
|
|
68
|
+
const times = new Float64Array(count);
|
|
69
|
+
const values = new Float64Array(count * CHANNELS);
|
|
70
|
+
for (let i = 0; i < count; i++) {
|
|
71
|
+
const key = keys[i] as CameraPathKey;
|
|
72
|
+
if (i > 0 && !(key.atSec > (times[i - 1] as number))) {
|
|
73
|
+
throw new Error(`camera path key ${i} at ${key.atSec}s must be later than the one before`);
|
|
74
|
+
}
|
|
75
|
+
times[i] = key.atSec;
|
|
76
|
+
values.set([...key.eye, ...key.target, key.fovDeg], i * CHANNELS);
|
|
77
|
+
}
|
|
78
|
+
const first = times[0] as number;
|
|
79
|
+
const last = times[count - 1] as number;
|
|
80
|
+
let durationSec = last;
|
|
81
|
+
if (options.loop) {
|
|
82
|
+
const period = options.periodSec ?? 0;
|
|
83
|
+
if (!(first + period > last)) {
|
|
84
|
+
throw new Error(`a looped camera path's period ${period}s must end after its last key`);
|
|
85
|
+
}
|
|
86
|
+
durationSec = period;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const tangents = new Float64Array(count * CHANNELS);
|
|
90
|
+
for (let i = 0; i < count; i++) {
|
|
91
|
+
let before = i - 1;
|
|
92
|
+
let after = i + 1;
|
|
93
|
+
let tBefore = before >= 0 ? (times[before] as number) : 0;
|
|
94
|
+
let tAfter = after < count ? (times[after] as number) : 0;
|
|
95
|
+
if (options.loop) {
|
|
96
|
+
if (before < 0) {
|
|
97
|
+
before = count - 1;
|
|
98
|
+
tBefore = (times[before] as number) - durationSec;
|
|
99
|
+
}
|
|
100
|
+
if (after >= count) {
|
|
101
|
+
after = 0;
|
|
102
|
+
tAfter = (times[0] as number) + durationSec;
|
|
103
|
+
}
|
|
104
|
+
} else {
|
|
105
|
+
/* An open end has one neighbour, and its tangent is the straight line to it. */
|
|
106
|
+
if (before < 0) {
|
|
107
|
+
before = i;
|
|
108
|
+
tBefore = times[i] as number;
|
|
109
|
+
}
|
|
110
|
+
if (after >= count) {
|
|
111
|
+
after = i;
|
|
112
|
+
tAfter = times[i] as number;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
const span = tAfter - tBefore;
|
|
116
|
+
for (let c = 0; c < CHANNELS; c++) {
|
|
117
|
+
const delta =
|
|
118
|
+
(values[after * CHANNELS + c] as number) - (values[before * CHANNELS + c] as number);
|
|
119
|
+
tangents[i * CHANNELS + c] = delta / span;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return { loop: options.loop, durationSec, count, times, values, tangents };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** The camera at `timeSec`. An open path holds its ends; a looped one wraps. */
|
|
126
|
+
export function sampleCameraPath(
|
|
127
|
+
path: CameraPath,
|
|
128
|
+
timeSec: number,
|
|
129
|
+
out: CameraPathSample,
|
|
130
|
+
): CameraPathSample {
|
|
131
|
+
const { times, values, tangents, count } = path;
|
|
132
|
+
const first = times[0] as number;
|
|
133
|
+
let t = timeSec;
|
|
134
|
+
if (path.loop) {
|
|
135
|
+
t = first + ((((timeSec - first) % path.durationSec) + path.durationSec) % path.durationSec);
|
|
136
|
+
} else if (t <= first) {
|
|
137
|
+
return write(out, values, 0);
|
|
138
|
+
} else if (t >= (times[count - 1] as number)) {
|
|
139
|
+
return write(out, values, count - 1);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
let i = 0;
|
|
143
|
+
while (i < count - 1 && (times[i + 1] as number) <= t) i++;
|
|
144
|
+
const next = i + 1 < count ? i + 1 : 0;
|
|
145
|
+
const t0 = times[i] as number;
|
|
146
|
+
const t1 = next === 0 ? first + path.durationSec : (times[next] as number);
|
|
147
|
+
const h = t1 - t0;
|
|
148
|
+
const u = (t - t0) / h;
|
|
149
|
+
const u2 = u * u;
|
|
150
|
+
const u3 = u2 * u;
|
|
151
|
+
const h00 = 2 * u3 - 3 * u2 + 1;
|
|
152
|
+
const h10 = u3 - 2 * u2 + u;
|
|
153
|
+
const h01 = -2 * u3 + 3 * u2;
|
|
154
|
+
const h11 = u3 - u2;
|
|
155
|
+
for (let c = 0; c < CHANNELS; c++) {
|
|
156
|
+
const v =
|
|
157
|
+
h00 * (values[i * CHANNELS + c] as number) +
|
|
158
|
+
h10 * h * (tangents[i * CHANNELS + c] as number) +
|
|
159
|
+
h01 * (values[next * CHANNELS + c] as number) +
|
|
160
|
+
h11 * h * (tangents[next * CHANNELS + c] as number);
|
|
161
|
+
if (c < 3) out.eye[c] = v;
|
|
162
|
+
else if (c < 6) out.target[c - 3] = v;
|
|
163
|
+
else out.fovDeg = v;
|
|
164
|
+
}
|
|
165
|
+
return out;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function write(out: CameraPathSample, values: Float64Array, key: number): CameraPathSample {
|
|
169
|
+
const at = key * CHANNELS;
|
|
170
|
+
out.eye[0] = values[at] as number;
|
|
171
|
+
out.eye[1] = values[at + 1] as number;
|
|
172
|
+
out.eye[2] = values[at + 2] as number;
|
|
173
|
+
out.target[0] = values[at + 3] as number;
|
|
174
|
+
out.target[1] = values[at + 4] as number;
|
|
175
|
+
out.target[2] = values[at + 5] as number;
|
|
176
|
+
out.fovDeg = values[at + 6] as number;
|
|
177
|
+
return out;
|
|
178
|
+
}
|