@driftengine/core 4.8.2 → 4.8.4
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 +127 -0
- package/README.md +1 -1
- package/dist/geometry/meshConcat.js +2 -0
- package/dist/index.d.ts +11 -4
- package/dist/index.js +22 -1
- package/dist/math/scalar.js +1 -1
- package/dist/render/anisotropicLobe.d.ts +7 -0
- package/dist/render/anisotropicLobe.js +33 -0
- package/dist/render/backend/api.d.ts +41 -4
- package/dist/render/backend/materialChanges.d.ts +7 -2
- package/dist/render/backend/materialChanges.js +8 -3
- package/dist/render/backend/select.js +8 -0
- package/dist/render/backend/webgl2/clothTextures.d.ts +27 -0
- package/dist/render/backend/webgl2/clothTextures.js +93 -0
- package/dist/render/backend/webgl2/renderer.d.ts +159 -3
- package/dist/render/backend/webgl2/renderer.js +662 -80
- package/dist/render/backend/webgl2/skinScatter.d.ts +50 -0
- package/dist/render/backend/webgl2/skinScatter.js +256 -0
- package/dist/render/backend/webgpu/buffers.d.ts +25 -11
- package/dist/render/backend/webgpu/buffers.js +98 -49
- package/dist/render/backend/webgpu/causticsPass.js +2 -1
- package/dist/render/backend/webgpu/clothSolvePack.d.ts +27 -0
- package/dist/render/backend/webgpu/clothSolvePack.js +129 -0
- package/dist/render/backend/webgpu/clothTextures.d.ts +68 -0
- package/dist/render/backend/webgpu/clothTextures.js +145 -0
- package/dist/render/backend/webgpu/compressedUpload.d.ts +18 -0
- package/dist/render/backend/webgpu/compressedUpload.js +38 -0
- package/dist/render/backend/webgpu/computeHost.d.ts +21 -0
- package/dist/render/backend/webgpu/computeHost.js +5 -0
- package/dist/render/backend/webgpu/depthPass.d.ts +29 -9
- package/dist/render/backend/webgpu/depthPass.js +43 -12
- package/dist/render/backend/webgpu/drawCommand.js +6 -2
- package/dist/render/backend/webgpu/filmPass.js +2 -1
- package/dist/render/backend/webgpu/flatPass.d.ts +43 -4
- package/dist/render/backend/webgpu/flatPass.js +111 -13
- package/dist/render/backend/webgpu/gpuSkinnedCloth.d.ts +74 -0
- package/dist/render/backend/webgpu/gpuSkinnedCloth.js +452 -0
- package/dist/render/backend/webgpu/linePass.js +2 -1
- package/dist/render/backend/webgpu/motionPass.d.ts +20 -0
- package/dist/render/backend/webgpu/motionPass.js +106 -9
- package/dist/render/backend/webgpu/panelPass.js +2 -1
- package/dist/render/backend/webgpu/particlePass.js +2 -1
- package/dist/render/backend/webgpu/plumePass.js +2 -1
- package/dist/render/backend/webgpu/postPass.d.ts +15 -3
- package/dist/render/backend/webgpu/postPass.js +25 -8
- package/dist/render/backend/webgpu/renderer.d.ts +123 -6
- package/dist/render/backend/webgpu/renderer.js +534 -58
- package/dist/render/backend/webgpu/sdfTextPass.js +2 -1
- package/dist/render/backend/webgpu/shaders/clothSolve.wgsl.d.ts +33 -0
- package/dist/render/backend/webgpu/shaders/clothSolve.wgsl.js +469 -0
- package/dist/render/backend/webgpu/skinScatterPass.d.ts +51 -0
- package/dist/render/backend/webgpu/skinScatterPass.js +253 -0
- package/dist/render/backend/webgpu/surfaceTexturePass.d.ts +4 -1
- package/dist/render/backend/webgpu/surfaceTexturePass.js +37 -12
- package/dist/render/backend/webgpu/textPass.js +2 -1
- package/dist/render/backend/webgpu/waterPass.js +2 -1
- package/dist/render/backend/webgpu/windStreakPass.js +2 -1
- package/dist/render/boltBatch.js +2 -1
- package/dist/render/causticsRenderer.js +2 -1
- package/dist/render/clothBindingData.d.ts +41 -0
- package/dist/render/clothBindingData.js +72 -0
- package/dist/render/clothPlacement.d.ts +25 -0
- package/dist/render/clothPlacement.js +85 -0
- package/dist/render/compressedSource.d.ts +66 -0
- package/dist/render/compressedSource.js +138 -0
- package/dist/render/cutoutCaster.d.ts +5 -1
- package/dist/render/cutoutCaster.js +6 -0
- package/dist/render/cutoutDither.d.ts +51 -0
- package/dist/render/cutoutDither.js +90 -0
- package/dist/render/decalPass.js +2 -1
- package/dist/render/eyeRefraction.d.ts +35 -0
- package/dist/render/eyeRefraction.js +40 -0
- package/dist/render/glCompressed.d.ts +27 -0
- package/dist/render/glCompressed.js +53 -0
- package/dist/render/globalMediumPass.js +3 -2
- package/dist/render/hairLobes.d.ts +70 -0
- package/dist/render/hairLobes.js +120 -0
- package/dist/render/lightBudget.d.ts +22 -0
- package/dist/render/lightBudget.js +22 -0
- package/dist/render/lineBatch.js +5 -1
- package/dist/render/mesh.d.ts +7 -0
- package/dist/render/mesh.js +24 -0
- package/dist/render/oitPass.js +2 -1
- package/dist/render/particleBatch.js +3 -2
- package/dist/render/particlePool.d.ts +6 -2
- package/dist/render/plumeRenderer.js +3 -2
- package/dist/render/recon/motionVectors.d.ts +4 -2
- package/dist/render/recon/motionVectors.js +6 -3
- package/dist/render/renderQuality.d.ts +29 -2
- package/dist/render/renderQuality.js +5 -0
- package/dist/render/rendererHarness.js +4 -0
- package/dist/render/sceneCoverage.d.ts +54 -0
- package/dist/render/sceneCoverage.js +72 -0
- package/dist/render/sceneTarget.d.ts +12 -0
- package/dist/render/sceneTarget.js +14 -0
- package/dist/render/sdfTextRenderer.js +2 -1
- package/dist/render/shaders/clothBinding.d.ts +22 -0
- package/dist/render/shaders/clothBinding.js +80 -0
- package/dist/render/shaders/depth.js +45 -8
- package/dist/render/shaders/flat/anisotropic.d.ts +15 -0
- package/dist/render/shaders/flat/anisotropic.js +68 -0
- package/dist/render/shaders/flat/eye.d.ts +1 -0
- package/dist/render/shaders/flat/eye.js +81 -0
- package/dist/render/shaders/flat/hair.d.ts +1 -0
- package/dist/render/shaders/flat/hair.js +101 -0
- package/dist/render/shaders/flat/index.js +42 -3
- package/dist/render/shaders/flat/main.d.ts +1 -1
- package/dist/render/shaders/flat/main.js +113 -17
- package/dist/render/shaders/flat/models.d.ts +14 -0
- package/dist/render/shaders/flat/models.js +273 -0
- package/dist/render/shaders/flat/preamble.js +26 -24
- package/dist/render/shaders/flat/skin.d.ts +1 -0
- package/dist/render/shaders/flat/skin.js +114 -0
- package/dist/render/shaders/generated/depth.wgsl.d.ts +66 -24
- package/dist/render/shaders/generated/depth.wgsl.js +66 -24
- package/dist/render/shaders/generated/flat.wgsl.d.ts +1815 -1704
- package/dist/render/shaders/generated/flat.wgsl.js +1853 -1742
- package/dist/render/shaders/generated/line.wgsl.d.ts +1 -1
- package/dist/render/shaders/generated/line.wgsl.js +1 -1
- package/dist/render/shaders/generated/particle.wgsl.d.ts +3 -3
- package/dist/render/shaders/generated/particle.wgsl.js +3 -3
- package/dist/render/shaders/generated/rush.wgsl.d.ts +1 -1
- package/dist/render/shaders/generated/rush.wgsl.js +1 -1
- package/dist/render/shaders/generated/sdfText.wgsl.d.ts +1 -1
- package/dist/render/shaders/generated/sdfText.wgsl.js +1 -1
- package/dist/render/shaders/generated/skinBlur.wgsl.d.ts +48 -0
- package/dist/render/shaders/generated/skinBlur.wgsl.js +54 -0
- 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/line.d.ts +1 -1
- package/dist/render/shaders/outputTransform.d.ts +14 -1
- package/dist/render/shaders/outputTransform.js +23 -1
- package/dist/render/shaders/particle.d.ts +3 -3
- package/dist/render/shaders/recon/motion.wgsl.d.ts +1 -1
- package/dist/render/shaders/recon/motion.wgsl.js +102 -0
- package/dist/render/shaders/recon/resolve.wgsl.js +15 -1
- package/dist/render/shaders/rush.js +13 -2
- package/dist/render/shaders/sdfText.d.ts +1 -1
- package/dist/render/shaders/skinBlur.d.ts +1 -0
- package/dist/render/shaders/skinBlur.js +103 -0
- package/dist/render/shaders/skinning.d.ts +3 -1
- package/dist/render/shaders/skinning.js +31 -4
- package/dist/render/shaders/temporalResolve.js +12 -5
- package/dist/render/shadowCasters.d.ts +13 -1
- package/dist/render/skinBlur.d.ts +41 -0
- package/dist/render/skinBlur.js +49 -0
- package/dist/render/skinScatter.d.ts +67 -0
- package/dist/render/skinScatter.js +80 -0
- package/dist/render/skinnedClothRun.d.ts +26 -0
- package/dist/render/skinnedClothRun.js +84 -0
- package/dist/render/ssrPass.js +2 -1
- package/dist/render/surfaceModel.d.ts +129 -0
- package/dist/render/surfaceModel.js +152 -0
- package/dist/render/surfaceTexture.d.ts +55 -4
- package/dist/render/surfaceTexture.js +60 -46
- package/dist/render/textRenderer.js +2 -1
- package/dist/render/vertexDefaults.js +8 -0
- package/dist/render/waterRenderer.js +2 -1
- package/dist/render/windStreakRenderer.js +2 -1
- package/package.json +6 -6
- package/src/geometry/meshConcat.ts +2 -0
- package/src/index.ts +53 -1
- package/src/math/scalar.ts +1 -1
- package/src/render/anisotropicLobe.ts +41 -0
- package/src/render/backend/api.ts +52 -3
- package/src/render/backend/materialChanges.ts +8 -3
- package/src/render/backend/select.ts +8 -0
- package/src/render/backend/webgl2/clothTextures.ts +120 -0
- package/src/render/backend/webgl2/renderer.ts +750 -80
- package/src/render/backend/webgl2/skinScatter.ts +309 -0
- package/src/render/backend/webgpu/buffers.ts +137 -63
- package/src/render/backend/webgpu/causticsPass.ts +2 -1
- package/src/render/backend/webgpu/clothSolvePack.ts +161 -0
- package/src/render/backend/webgpu/clothTextures.ts +194 -0
- package/src/render/backend/webgpu/compressedUpload.ts +65 -0
- package/src/render/backend/webgpu/computeHost.ts +25 -0
- package/src/render/backend/webgpu/depthPass.ts +43 -10
- package/src/render/backend/webgpu/drawCommand.ts +6 -2
- package/src/render/backend/webgpu/filmPass.ts +2 -1
- package/src/render/backend/webgpu/flatPass.ts +147 -8
- package/src/render/backend/webgpu/gpuSkinnedCloth.ts +502 -0
- package/src/render/backend/webgpu/linePass.ts +2 -1
- package/src/render/backend/webgpu/motionPass.ts +128 -9
- package/src/render/backend/webgpu/panelPass.ts +2 -1
- package/src/render/backend/webgpu/particlePass.ts +2 -1
- package/src/render/backend/webgpu/plumePass.ts +2 -1
- package/src/render/backend/webgpu/postPass.ts +25 -6
- package/src/render/backend/webgpu/renderer.ts +701 -52
- package/src/render/backend/webgpu/sdfTextPass.ts +2 -1
- package/src/render/backend/webgpu/shaders/clothSolve.wgsl.ts +470 -0
- package/src/render/backend/webgpu/skinScatterPass.ts +309 -0
- package/src/render/backend/webgpu/surfaceTexturePass.ts +42 -12
- package/src/render/backend/webgpu/textPass.ts +2 -1
- package/src/render/backend/webgpu/waterPass.ts +2 -1
- package/src/render/backend/webgpu/windStreakPass.ts +2 -1
- package/src/render/boltBatch.ts +2 -1
- package/src/render/causticsRenderer.ts +2 -1
- package/src/render/clothBindingData.ts +89 -0
- package/src/render/clothPlacement.ts +114 -0
- package/src/render/compressedSource.ts +198 -0
- package/src/render/cutoutCaster.ts +20 -2
- package/src/render/cutoutDither.ts +99 -0
- package/src/render/decalPass.ts +2 -1
- package/src/render/eyeRefraction.ts +72 -0
- package/src/render/glCompressed.ts +86 -0
- package/src/render/globalMediumPass.ts +3 -2
- package/src/render/hairLobes.ts +188 -0
- package/src/render/lightBudget.ts +25 -0
- package/src/render/lineBatch.ts +3 -1
- package/src/render/mesh.ts +23 -0
- package/src/render/oitPass.ts +2 -1
- package/src/render/particleBatch.ts +3 -2
- package/src/render/particlePool.ts +6 -2
- package/src/render/plumeRenderer.ts +3 -2
- package/src/render/recon/motionVectors.ts +6 -3
- package/src/render/renderQuality.ts +37 -2
- package/src/render/rendererHarness.ts +4 -0
- package/src/render/sceneCoverage.ts +78 -0
- package/src/render/sceneTarget.ts +21 -0
- package/src/render/sdfTextRenderer.ts +2 -1
- package/src/render/shaders/clothBinding.ts +81 -0
- package/src/render/shaders/depth.ts +45 -8
- package/src/render/shaders/flat/anisotropic.ts +68 -0
- package/src/render/shaders/flat/eye.ts +82 -0
- package/src/render/shaders/flat/hair.ts +102 -0
- package/src/render/shaders/flat/index.ts +42 -3
- package/src/render/shaders/flat/main.ts +113 -17
- package/src/render/shaders/flat/models.ts +286 -0
- package/src/render/shaders/flat/preamble.ts +26 -24
- package/src/render/shaders/flat/skin.ts +116 -0
- package/src/render/shaders/generated/depth.wgsl.ts +66 -24
- package/src/render/shaders/generated/flat.wgsl.ts +1853 -1742
- package/src/render/shaders/generated/line.wgsl.ts +1 -1
- package/src/render/shaders/generated/particle.wgsl.ts +3 -3
- package/src/render/shaders/generated/rush.wgsl.ts +1 -1
- package/src/render/shaders/generated/sdfText.wgsl.ts +1 -1
- package/src/render/shaders/generated/skinBlur.wgsl.ts +56 -0
- package/src/render/shaders/generated/temporalResolve.wgsl.ts +1 -1
- package/src/render/shaders/outputTransform.ts +24 -1
- package/src/render/shaders/recon/motion.wgsl.ts +102 -0
- package/src/render/shaders/recon/resolve.wgsl.ts +15 -1
- package/src/render/shaders/rush.ts +13 -2
- package/src/render/shaders/skinBlur.ts +105 -0
- package/src/render/shaders/skinning.ts +31 -4
- package/src/render/shaders/temporalResolve.ts +12 -5
- package/src/render/shadowCasters.ts +13 -0
- package/src/render/skinBlur.ts +53 -0
- package/src/render/skinScatter.ts +92 -0
- package/src/render/skinnedClothRun.ts +128 -0
- package/src/render/ssrPass.ts +2 -1
- package/src/render/surfaceModel.ts +274 -0
- package/src/render/surfaceTexture.ts +134 -63
- package/src/render/textRenderer.ts +2 -1
- package/src/render/vertexDefaults.ts +8 -0
- package/src/render/waterRenderer.ts +2 -1
- package/src/render/windStreakRenderer.ts +2 -1
- package/dist/math/exact.d.ts +0 -76
- package/dist/math/exact.js +0 -327
- package/src/math/exact.ts +0 -340
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where a cloth-bound vertex goes: the reference for what the vertex stage computes.
|
|
3
|
+
*
|
|
4
|
+
* A render vertex bound to a simulation triangle (particles a, b, c) sits at its barycentric point
|
|
5
|
+
* — `a·(1 − u − v) + b·u + c·v` — raised `offset` along the triangle's normal; its normal turns as
|
|
6
|
+
* the triangle turned since rest. The turn is the rotation from the triangle's rest frame
|
|
7
|
+
* to its frame now, each frame built from its first edge and its normal, so a vertex beside the
|
|
8
|
+
* triangle, a fold's width away, moves as the cloth moves rather than as its skeleton does.
|
|
9
|
+
*
|
|
10
|
+
* **A vertex bound to one particle** — all three indices equal — sits on it and keeps its normal:
|
|
11
|
+
* a single point has no frame to turn by. That is how a binding says "follow this particle".
|
|
12
|
+
*
|
|
13
|
+
* **A reference, as `recon/motionVectors.ts` is**: nothing at run time calls it. The vertex stage in
|
|
14
|
+
* `shaders/clothBinding.ts` and the cloth motion stage in `shaders/recon/motion.wgsl.ts` are the
|
|
15
|
+
* same arithmetic for the device, and nothing but a device holds them to this — the source tests
|
|
16
|
+
* pin where the shader moves a vertex, not the numbers it moves it by. `demo/dev/skinnedCloth.html`
|
|
17
|
+
* is where the two backends were compared, and they agree to two levels in 255.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/** Where a placement lands, written into caller-owned arrays. */
|
|
21
|
+
export interface ClothPlacement {
|
|
22
|
+
readonly position: Float64Array;
|
|
23
|
+
readonly normal: Float64Array;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const NOW = new Float64Array(9);
|
|
27
|
+
const REST = new Float64Array(9);
|
|
28
|
+
|
|
29
|
+
/** A triangle's frame — first edge, normal × edge, normal — as three columns, into `out`. */
|
|
30
|
+
function frame(
|
|
31
|
+
particles: ArrayLike<number>,
|
|
32
|
+
a: number,
|
|
33
|
+
b: number,
|
|
34
|
+
c: number,
|
|
35
|
+
out: Float64Array,
|
|
36
|
+
): void {
|
|
37
|
+
const ex = (particles[b * 3] as number) - (particles[a * 3] as number);
|
|
38
|
+
const ey = (particles[b * 3 + 1] as number) - (particles[a * 3 + 1] as number);
|
|
39
|
+
const ez = (particles[b * 3 + 2] as number) - (particles[a * 3 + 2] as number);
|
|
40
|
+
const fx = (particles[c * 3] as number) - (particles[a * 3] as number);
|
|
41
|
+
const fy = (particles[c * 3 + 1] as number) - (particles[a * 3 + 1] as number);
|
|
42
|
+
const fz = (particles[c * 3 + 2] as number) - (particles[a * 3 + 2] as number);
|
|
43
|
+
let nx = ey * fz - ez * fy;
|
|
44
|
+
let ny = ez * fx - ex * fz;
|
|
45
|
+
let nz = ex * fy - ey * fx;
|
|
46
|
+
const nl = Math.sqrt(nx * nx + ny * ny + nz * nz) || 1;
|
|
47
|
+
nx /= nl;
|
|
48
|
+
ny /= nl;
|
|
49
|
+
nz /= nl;
|
|
50
|
+
const el = Math.sqrt(ex * ex + ey * ey + ez * ez) || 1;
|
|
51
|
+
const tx = ex / el;
|
|
52
|
+
const ty = ey / el;
|
|
53
|
+
const tz = ez / el;
|
|
54
|
+
out[0] = tx;
|
|
55
|
+
out[1] = ty;
|
|
56
|
+
out[2] = tz;
|
|
57
|
+
out[3] = ny * tz - nz * ty;
|
|
58
|
+
out[4] = nz * tx - nx * tz;
|
|
59
|
+
out[5] = nx * ty - ny * tx;
|
|
60
|
+
out[6] = nx;
|
|
61
|
+
out[7] = ny;
|
|
62
|
+
out[8] = nz;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Place one vertex: `now` and `rest` are particle positions, three floats each. */
|
|
66
|
+
export function clothPlace(
|
|
67
|
+
now: ArrayLike<number>,
|
|
68
|
+
rest: ArrayLike<number>,
|
|
69
|
+
a: number,
|
|
70
|
+
b: number,
|
|
71
|
+
c: number,
|
|
72
|
+
u: number,
|
|
73
|
+
v: number,
|
|
74
|
+
offset: number,
|
|
75
|
+
restNormal: readonly [number, number, number],
|
|
76
|
+
out: ClothPlacement,
|
|
77
|
+
): void {
|
|
78
|
+
if (a === b && b === c) {
|
|
79
|
+
out.position[0] = now[a * 3] as number;
|
|
80
|
+
out.position[1] = now[a * 3 + 1] as number;
|
|
81
|
+
out.position[2] = now[a * 3 + 2] as number;
|
|
82
|
+
out.normal[0] = restNormal[0];
|
|
83
|
+
out.normal[1] = restNormal[1];
|
|
84
|
+
out.normal[2] = restNormal[2];
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
frame(now, a, b, c, NOW);
|
|
88
|
+
frame(rest, a, b, c, REST);
|
|
89
|
+
const w = 1 - u - v;
|
|
90
|
+
for (let k = 0; k < 3; k++) {
|
|
91
|
+
out.position[k] =
|
|
92
|
+
(now[a * 3 + k] as number) * w +
|
|
93
|
+
(now[b * 3 + k] as number) * u +
|
|
94
|
+
(now[c * 3 + k] as number) * v +
|
|
95
|
+
(NOW[6 + k] as number) * offset;
|
|
96
|
+
}
|
|
97
|
+
/* Turned by now × restᵀ: the rest normal in the rest frame's terms, rebuilt in the frame now. */
|
|
98
|
+
const t =
|
|
99
|
+
(REST[0] as number) * restNormal[0] +
|
|
100
|
+
(REST[1] as number) * restNormal[1] +
|
|
101
|
+
(REST[2] as number) * restNormal[2];
|
|
102
|
+
const s =
|
|
103
|
+
(REST[3] as number) * restNormal[0] +
|
|
104
|
+
(REST[4] as number) * restNormal[1] +
|
|
105
|
+
(REST[5] as number) * restNormal[2];
|
|
106
|
+
const n =
|
|
107
|
+
(REST[6] as number) * restNormal[0] +
|
|
108
|
+
(REST[7] as number) * restNormal[1] +
|
|
109
|
+
(REST[8] as number) * restNormal[2];
|
|
110
|
+
for (let k = 0; k < 3; k++) {
|
|
111
|
+
out.normal[k] =
|
|
112
|
+
(NOW[k] as number) * t + (NOW[3 + k] as number) * s + (NOW[6 + k] as number) * n;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A block-compressed image a caller hands to `createSurfaceTexture`, and the one decision about it
|
|
3
|
+
* both backends share: whether this device takes it as blocks.
|
|
4
|
+
*
|
|
5
|
+
* **Uploaded as it is, or not at all.** A BC texture costs a byte a texel on the GPU (BC7) or half
|
|
6
|
+
* of one (BC1) against four for RGBA, and keeps the mip chain its author built rather than one a box
|
|
7
|
+
* filter makes. A device that cannot sample the format — most phones, which have ASTC and ETC2 —
|
|
8
|
+
* gets nothing from the blocks, and the engine does not decode them here: core ships no decoder.
|
|
9
|
+
* The loader in `@driftengine/assets` asks `uploadsCompressed` first and decodes at load where the
|
|
10
|
+
* answer is no; a caller handing a source this device cannot take is refused by name.
|
|
11
|
+
*
|
|
12
|
+
* Backend-neutral, because both backends must agree on what they accept before either touches a
|
|
13
|
+
* device. `BlockFormat` is the same union `@driftengine/drft` names `BcFormat`; core imports nothing
|
|
14
|
+
* from that package, and a format added there without a twin here fails where the loader passes one
|
|
15
|
+
* to the other, at compile time.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/** BC1 to BC5 and BC7. BC6H and the signed forms are not among them. */
|
|
19
|
+
export type BlockFormat = 'bc1' | 'bc2' | 'bc3' | 'bc4' | 'bc5' | 'bc7';
|
|
20
|
+
|
|
21
|
+
/** A format as a device offers it: the colour space is part of the name, as WebGPU's are. */
|
|
22
|
+
export type CompressedTextureFormat =
|
|
23
|
+
'bc1' | 'bc1-srgb' | 'bc2' | 'bc2-srgb' | 'bc3' | 'bc3-srgb' | 'bc4' | 'bc5' | 'bc7' | 'bc7-srgb';
|
|
24
|
+
|
|
25
|
+
/** Block-compressed texels with their stored mip chain, level 0 first. */
|
|
26
|
+
export interface CompressedTextureSource {
|
|
27
|
+
readonly format: BlockFormat;
|
|
28
|
+
readonly width: number;
|
|
29
|
+
readonly height: number;
|
|
30
|
+
readonly levels: readonly Uint8Array[];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** What `createSurfaceTexture` takes: an image, or blocks. */
|
|
34
|
+
export type SurfaceSource = TexImageSource | CompressedTextureSource;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The layers as blocks, or null where every one is an image. An array mixing the two is refused:
|
|
38
|
+
* one GPU texture has one format.
|
|
39
|
+
*/
|
|
40
|
+
export function compressedLayers(
|
|
41
|
+
sources: readonly SurfaceSource[],
|
|
42
|
+
): readonly CompressedTextureSource[] | null {
|
|
43
|
+
let count = 0;
|
|
44
|
+
for (const source of sources) if (isCompressedSource(source)) count++;
|
|
45
|
+
if (count === 0) return null;
|
|
46
|
+
if (count !== sources.length) {
|
|
47
|
+
throw new Error(
|
|
48
|
+
'createSurfaceTextureArray: some layers are blocks and some are images, and one texture has ' +
|
|
49
|
+
'one format. Decode the blocks, or build the array from blocks alone.',
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
return sources as readonly CompressedTextureSource[];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Bytes per 4x4 block. */
|
|
56
|
+
export const BLOCK_BYTES: Readonly<Record<BlockFormat, number>> = {
|
|
57
|
+
bc1: 8,
|
|
58
|
+
bc2: 16,
|
|
59
|
+
bc3: 16,
|
|
60
|
+
bc4: 8,
|
|
61
|
+
bc5: 16,
|
|
62
|
+
bc7: 16,
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
/** Whether `source` is a compressed source rather than an image. */
|
|
66
|
+
export function isCompressedSource(source: unknown): source is CompressedTextureSource {
|
|
67
|
+
const candidate = source as { format?: unknown; levels?: unknown } | null;
|
|
68
|
+
return (
|
|
69
|
+
typeof candidate === 'object' &&
|
|
70
|
+
candidate !== null &&
|
|
71
|
+
typeof candidate.format === 'string' &&
|
|
72
|
+
Array.isArray(candidate.levels)
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** The format a device would have to offer, or null for one with no sRGB form. */
|
|
77
|
+
export function compressedFormatName(
|
|
78
|
+
format: BlockFormat,
|
|
79
|
+
srgb: boolean,
|
|
80
|
+
): CompressedTextureFormat | null {
|
|
81
|
+
if (!srgb) return format;
|
|
82
|
+
return format === 'bc4' || format === 'bc5' ? null : `${format}-srgb`;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Whether a `width` by `height` source in `format` uploads as blocks on a device offering
|
|
87
|
+
* `available`. Level 0 must be whole blocks each way: WebGPU refuses any other size for a BC
|
|
88
|
+
* texture, and WebGL2's S3TC extension says the same, so a 6x6 image is decoded rather than
|
|
89
|
+
* refused. What would make that wrong is a device that pads; none is relied on here.
|
|
90
|
+
*/
|
|
91
|
+
export function uploadsCompressed(
|
|
92
|
+
format: BlockFormat,
|
|
93
|
+
srgb: boolean,
|
|
94
|
+
width: number,
|
|
95
|
+
height: number,
|
|
96
|
+
available: readonly CompressedTextureFormat[],
|
|
97
|
+
): boolean {
|
|
98
|
+
const name = compressedFormatName(format, srgb);
|
|
99
|
+
return name !== null && width % 4 === 0 && height % 4 === 0 && available.includes(name);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** The bytes level `level` of a `width` by `height` image takes: whole blocks, one at least. */
|
|
103
|
+
function levelBytes(format: BlockFormat, width: number, height: number, level: number): number {
|
|
104
|
+
const w = Math.max(1, width >> level);
|
|
105
|
+
const h = Math.max(1, height >> level);
|
|
106
|
+
return Math.ceil(w / 4) * Math.ceil(h / 4) * BLOCK_BYTES[format];
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* How many levels the layers carry, after checking each is the size its place requires and every
|
|
111
|
+
* layer matches the first. Refuses by name: a short level uploaded anyway is a device error at
|
|
112
|
+
* submit, which on WebGPU is a frame that draws nothing and says so somewhere else.
|
|
113
|
+
*/
|
|
114
|
+
export function compressedLevels(layers: readonly CompressedTextureSource[]): number {
|
|
115
|
+
const first = layers[0];
|
|
116
|
+
if (first === undefined)
|
|
117
|
+
throw new Error('createSurfaceTexture: an array needs at least one image');
|
|
118
|
+
const { format, width, height } = first;
|
|
119
|
+
const count = first.levels.length;
|
|
120
|
+
const most = Math.floor(Math.log2(Math.max(1, width, height))) + 1;
|
|
121
|
+
if (count < 1 || count > most) {
|
|
122
|
+
throw new Error(
|
|
123
|
+
`createSurfaceTexture: ${count} levels for a ${width}x${height} image, which has at least 1 ` +
|
|
124
|
+
`and at most ${most}`,
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
for (let layer = 0; layer < layers.length; layer++) {
|
|
128
|
+
const source = layers[layer] as CompressedTextureSource;
|
|
129
|
+
if (
|
|
130
|
+
source.format !== format ||
|
|
131
|
+
source.width !== width ||
|
|
132
|
+
source.height !== height ||
|
|
133
|
+
source.levels.length !== count
|
|
134
|
+
) {
|
|
135
|
+
throw new Error(
|
|
136
|
+
`createSurfaceTexture: layer ${layer} is ${source.width}x${source.height} ` +
|
|
137
|
+
`${source.format.toUpperCase()} with ${source.levels.length} levels, and layer 0 is ` +
|
|
138
|
+
`${width}x${height} ${format.toUpperCase()} with ${count}. Every layer of an array is one ` +
|
|
139
|
+
'format, size and chain on the device.',
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
for (let level = 0; level < count; level++) {
|
|
143
|
+
const want = levelBytes(format, width, height, level);
|
|
144
|
+
const got = (source.levels[level] as Uint8Array).length;
|
|
145
|
+
if (got !== want) {
|
|
146
|
+
throw new Error(
|
|
147
|
+
`createSurfaceTexture: level ${level} of a ${width}x${height} ${format.toUpperCase()} ` +
|
|
148
|
+
`image is ${want} bytes, and ${got} were given`,
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return count;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** What a refusal names: the format, its colour space, and what the device offers instead. */
|
|
157
|
+
export function compressedRefusal(
|
|
158
|
+
format: BlockFormat,
|
|
159
|
+
srgb: boolean,
|
|
160
|
+
width: number,
|
|
161
|
+
height: number,
|
|
162
|
+
available: readonly CompressedTextureFormat[],
|
|
163
|
+
): string {
|
|
164
|
+
const asked = `${format.toUpperCase()}${srgb ? ' sRGB' : ''} at ${width}x${height}`;
|
|
165
|
+
const offered = available.length === 0 ? 'no BC format' : available.join(', ');
|
|
166
|
+
return (
|
|
167
|
+
`createSurfaceTexture: this device cannot take ${asked} as blocks — it offers ${offered}, and ` +
|
|
168
|
+
'a BC texture must be whole 4x4 blocks at level 0. Decode it first (decodeBc in ' +
|
|
169
|
+
'@driftengine/assets), or ask renderer.compressedFormats before choosing.'
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** The format name and level count blocks upload as here, or a refusal naming why they cannot. */
|
|
174
|
+
export function planBlocks(
|
|
175
|
+
blocks: readonly CompressedTextureSource[],
|
|
176
|
+
srgb: boolean,
|
|
177
|
+
compressed: readonly CompressedTextureFormat[],
|
|
178
|
+
): { readonly name: CompressedTextureFormat; readonly levels: number } {
|
|
179
|
+
const { format, width, height } = blocks[0] as CompressedTextureSource;
|
|
180
|
+
const name = compressedFormatName(format, srgb);
|
|
181
|
+
if (name === null || !uploadsCompressed(format, srgb, width, height, compressed)) {
|
|
182
|
+
throw new Error(compressedRefusal(format, srgb, width, height, compressed));
|
|
183
|
+
}
|
|
184
|
+
return { name, levels: compressedLevels(blocks) };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* `update` replaces pixels with an image, and a texture holding blocks has no pixels to replace:
|
|
189
|
+
* its format is the blocks', and an image would be a different texture. Refused rather than
|
|
190
|
+
* quietly reallocated, so the handle a draw holds keeps meaning what it meant.
|
|
191
|
+
*/
|
|
192
|
+
export function refuseBlockUpdate(format: BlockFormat | null): void {
|
|
193
|
+
if (format === null) return;
|
|
194
|
+
throw new Error(
|
|
195
|
+
`updateSurfaceTexture: this texture holds ${format.toUpperCase()} blocks, and update replaces ` +
|
|
196
|
+
'an image. Build a new texture for new contents.',
|
|
197
|
+
);
|
|
198
|
+
}
|
|
@@ -13,19 +13,34 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import type { SurfaceMaterial } from './surfaceTexture.ts';
|
|
15
15
|
|
|
16
|
-
/** A cutout caster's texture, its cutoff and its UV scale. */
|
|
16
|
+
/** A cutout caster's texture, its cutoff and its UV scale and offset. */
|
|
17
17
|
export interface CutoutCaster<Texture> {
|
|
18
18
|
readonly albedo: Texture;
|
|
19
19
|
readonly cutoff: number;
|
|
20
|
+
/** 1 for a dithered edge, which a caster draws with a pattern that does not move. */
|
|
21
|
+
readonly dithered: number;
|
|
20
22
|
readonly u: number;
|
|
21
23
|
readonly v: number;
|
|
24
|
+
readonly uOffset: number;
|
|
25
|
+
readonly vOffset: number;
|
|
22
26
|
}
|
|
23
27
|
|
|
24
|
-
const answer: {
|
|
28
|
+
const answer: {
|
|
29
|
+
albedo: unknown;
|
|
30
|
+
cutoff: number;
|
|
31
|
+
dithered: number;
|
|
32
|
+
u: number;
|
|
33
|
+
v: number;
|
|
34
|
+
uOffset: number;
|
|
35
|
+
vOffset: number;
|
|
36
|
+
} = {
|
|
25
37
|
albedo: null,
|
|
26
38
|
cutoff: 0,
|
|
39
|
+
dithered: 0,
|
|
27
40
|
u: 1,
|
|
28
41
|
v: 1,
|
|
42
|
+
uOffset: 0,
|
|
43
|
+
vOffset: 0,
|
|
29
44
|
};
|
|
30
45
|
|
|
31
46
|
/** The cutout `material` casts, or null for an ordinary caster. */
|
|
@@ -38,7 +53,10 @@ export function cutoutOf<Texture>(
|
|
|
38
53
|
if (albedo === null || !(cutoff > 0)) return null;
|
|
39
54
|
answer.albedo = albedo;
|
|
40
55
|
answer.cutoff = cutoff;
|
|
56
|
+
answer.dithered = material.cutoutMode === 'dithered' ? 1 : 0;
|
|
41
57
|
answer.u = material.uScale ?? 1;
|
|
42
58
|
answer.v = material.vScale ?? 1;
|
|
59
|
+
answer.uOffset = material.uOffset ?? 0;
|
|
60
|
+
answer.vOffset = material.vOffset ?? 0;
|
|
43
61
|
return answer as CutoutCaster<Texture>;
|
|
44
62
|
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A cutout whose edge is a share of the pixel rather than a line through it.
|
|
3
|
+
*
|
|
4
|
+
* **Why.** A hard alpha test draws a strand of hair, an eyelash or a fringe of leaves as a stair of
|
|
5
|
+
* whole pixels: a card's alpha falls off over a few texels and the test turns that into on and off.
|
|
6
|
+
* A dithered cutout keeps each pixel with a probability, the share of it the texture covers, and
|
|
7
|
+
* something downstream then averages the pattern back into coverage. What can average it depends on
|
|
8
|
+
* the frame, so the material asks for the smooth edge and the renderer decides, per frame, how:
|
|
9
|
+
*
|
|
10
|
+
* - **A temporal resolve (TAA or DriftTR)**: the pixel is kept against a threshold that moves every
|
|
11
|
+
* frame, and the resolve integrates the pattern over its history into coverage.
|
|
12
|
+
* - **A multisampled frame**: the share goes to the GPU as alpha-to-coverage, which turns it into
|
|
13
|
+
* that many of the pixel's samples.
|
|
14
|
+
* - **Neither**: the hard test, because a dither nothing resolves is grain on the screen.
|
|
15
|
+
*
|
|
16
|
+
* **What it gives up**: a dithered edge under a temporal resolve is soft by one history's worth of
|
|
17
|
+
* blur and ghosts slightly behind a fast card, which is the resolve's own trade; and under
|
|
18
|
+
* multisampling the share is quantised to the sample count, five levels at four samples. **What
|
|
19
|
+
* would make it wrong** is a pass that tests depth for equality against the main pass and discards
|
|
20
|
+
* differently from it; the caster and the lit pass take their keep from the same two functions here.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/** What a material asks for. `'hard'` is every cutout that shipped before this. */
|
|
24
|
+
export type CutoutMode = 'hard' | 'dithered';
|
|
25
|
+
|
|
26
|
+
/** What the frame does with it: 0, 1 and 2 in the shader's `uCutout.y`. */
|
|
27
|
+
export type CutoutResolve = 'hard' | 'dither' | 'coverage';
|
|
28
|
+
|
|
29
|
+
/** The shader's number for a resolve. */
|
|
30
|
+
export const CUTOUT_RESOLVE_CODE: Readonly<Record<CutoutResolve, number>> = Object.freeze({
|
|
31
|
+
hard: 0,
|
|
32
|
+
dither: 1,
|
|
33
|
+
coverage: 2,
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* How a cutout is drawn this frame. A temporal resolve is preferred over multisampling when a frame
|
|
38
|
+
* has both, because it resolves every share and multisampling only the sample count's.
|
|
39
|
+
*/
|
|
40
|
+
export function resolveCutout(mode: CutoutMode, temporal: boolean, samples: number): CutoutResolve {
|
|
41
|
+
if (mode !== 'dithered') return 'hard';
|
|
42
|
+
if (temporal) return 'dither';
|
|
43
|
+
if (samples > 1) return 'coverage';
|
|
44
|
+
return 'hard';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* The share of a pixel a cutout keeps, from its alpha: half at the cutoff, ramping over a band
|
|
49
|
+
* twice the distance to the nearer end. At a cutoff of a half that is alpha itself.
|
|
50
|
+
*/
|
|
51
|
+
export function cutoutShare(alpha: number, cutoff: number): number {
|
|
52
|
+
const band = Math.max(2 * Math.min(cutoff, 1 - cutoff), 1e-4);
|
|
53
|
+
return Math.min(Math.max((alpha - cutoff) / band + 0.5, 0), 1);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* The temporal step of the noise below, per frame: Jimenez's constant (2014), which moves the
|
|
58
|
+
* pattern across the tile rather than stepping each pixel's threshold.
|
|
59
|
+
*
|
|
60
|
+
* **A golden-ratio step per pixel was tried and measured, and it is not better.** It spreads a still
|
|
61
|
+
* pixel's thresholds evenly over time, which reads like the cure for grain; on a card of strands
|
|
62
|
+
* under TAA it left 12.9 levels of grain against this one's 11.7. The floor is the resolve's own: it
|
|
63
|
+
* keeps nine tenths of its history, so the newest frame's kept-or-not pattern is a tenth of the
|
|
64
|
+
* picture, about ten levels of grain at a half share whatever sequence chose it.
|
|
65
|
+
*/
|
|
66
|
+
const NOISE_FRAME_STEP = 5.588238;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* The threshold a pixel's share is compared against: interleaved gradient noise (Jimenez,
|
|
70
|
+
* "Next Generation Post Processing in Call of Duty: Advanced Warfare", 2014), offset by the frame so
|
|
71
|
+
* the pattern moves. `x` and `y` are pixel centres, as `gl_FragCoord.xy` is.
|
|
72
|
+
*/
|
|
73
|
+
export function ditherThreshold(x: number, y: number, frame: number): number {
|
|
74
|
+
const px = x + NOISE_FRAME_STEP * frame;
|
|
75
|
+
const py = y + NOISE_FRAME_STEP * frame;
|
|
76
|
+
const inner = 0.06711056 * px + 0.00583715 * py;
|
|
77
|
+
const f = 52.9829189 * (inner - Math.floor(inner));
|
|
78
|
+
return f - Math.floor(f);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* The two functions in GLSL, for the lit pass and every caster that keeps the same pixels.
|
|
83
|
+
*
|
|
84
|
+
* `cutoutKeeps(share, pixel, frame)` is the dithered test; `cutoutShare(alpha, cutoff)` is also what
|
|
85
|
+
* a coverage frame writes as alpha. Frame is a float the renderer counts and wraps, so the noise's
|
|
86
|
+
* argument stays small enough for a half-precision device.
|
|
87
|
+
*/
|
|
88
|
+
export const CUTOUT_DITHER_GLSL = `
|
|
89
|
+
float cutoutShare(float alpha, float cutoff) {
|
|
90
|
+
float band = max(2.0 * min(cutoff, 1.0 - cutoff), 1e-4);
|
|
91
|
+
return clamp((alpha - cutoff) / band + 0.5, 0.0, 1.0);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
bool cutoutKeeps(float share, vec2 pixel, float frame) {
|
|
95
|
+
vec2 p = pixel + vec2(${NOISE_FRAME_STEP.toFixed(6)} * frame);
|
|
96
|
+
float noise = fract(52.9829189 * fract(dot(p, vec2(0.06711056, 0.00583715))));
|
|
97
|
+
return share > noise;
|
|
98
|
+
}
|
|
99
|
+
`;
|
package/src/render/decalPass.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { DEPTH_01_TO_CLIP } from './lightVolumeDraw.ts';
|
|
|
7
7
|
import { compileProgram, uniformLocations } from './shader.ts';
|
|
8
8
|
import { DECAL_PROJECT_FRAG } from './shaders/decalProject.ts';
|
|
9
9
|
import { FULLSCREEN_VERT } from './shaders/fullscreen.ts';
|
|
10
|
+
import { blendKeeping } from './sceneCoverage.ts';
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* Drawn decals on WebGL2: one scissored triangle per mark, multiplied into the finished scene.
|
|
@@ -66,7 +67,7 @@ export class DecalPass {
|
|
|
66
67
|
gl.depthMask(false);
|
|
67
68
|
gl.disable(gl.CULL_FACE);
|
|
68
69
|
gl.enable(gl.BLEND);
|
|
69
|
-
|
|
70
|
+
blendKeeping(gl, gl.ZERO, gl.SRC_COLOR);
|
|
70
71
|
gl.enable(gl.SCISSOR_TEST);
|
|
71
72
|
|
|
72
73
|
/* Unit 0, chosen rather than inherited: `bindTexture` binds to whichever unit is active, and
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The eye model's refraction, as `shaders/flat/eye.ts` computes it: the reference its tests hold the
|
|
3
|
+
* arithmetic to, and nothing at run time calls it.
|
|
4
|
+
*
|
|
5
|
+
* **An iris is seen through a cornea**, a few millimetres of clear fluid above it with an index near
|
|
6
|
+
* water's, so it is not where it is painted: the eye's ray bends at the cornea toward the axis and
|
|
7
|
+
* meets the iris plane somewhere else, and the iris seems to lie deeper and to shift as the eye
|
|
8
|
+
* turns away. The lit stage moves the iris's texture coordinate by that shift before any map of
|
|
9
|
+
* the material is read, so the whole iris — colour, relief, its own channels — moves as one.
|
|
10
|
+
*
|
|
11
|
+
* **The shift**: refract the view at the cornea's surface, follow the refracted ray down until it
|
|
12
|
+
* has descended the cornea's height above the iris plane along the eye's axis, and take what it
|
|
13
|
+
* travelled across the plane. Head-on it is nothing; at an angle θ to a flat cornea it is
|
|
14
|
+
* `height · tan(asin(sin θ / ior))`. The plane is perpendicular to the axis, so a cornea seen
|
|
15
|
+
* almost edge-on sends the ray along it: the descent is floored, which bounds the shift at about a
|
|
16
|
+
* thousand times the height rather than letting it run to infinity at the limb.
|
|
17
|
+
*/
|
|
18
|
+
import type { Vec3 } from '../math/color.ts';
|
|
19
|
+
|
|
20
|
+
/** Schlick's base for the cornea's highlight, at the model's default index 1.336. */
|
|
21
|
+
export const CORNEA_F0 = ((1 - 1.336) / (1 + 1.336)) ** 2;
|
|
22
|
+
|
|
23
|
+
/** How slowly a refracted ray may descend toward the iris plane before it is held there. */
|
|
24
|
+
export const EYE_MIN_DESCENT = 1e-3;
|
|
25
|
+
|
|
26
|
+
/** GLSL's `refract`, into `out`: the transmitted direction, or zeros past total internal reflection. */
|
|
27
|
+
export function refract(incident: Vec3, normal: Vec3, eta: number, out: Float64Array): void {
|
|
28
|
+
const d = normal[0] * incident[0] + normal[1] * incident[1] + normal[2] * incident[2];
|
|
29
|
+
const k = 1 - eta * eta * (1 - d * d);
|
|
30
|
+
if (k < 0) {
|
|
31
|
+
out.fill(0);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
const s = eta * d + Math.sqrt(k);
|
|
35
|
+
for (let c = 0; c < 3; c++) out[c] = eta * (incident[c] as number) - s * (normal[c] as number);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* How far across the iris plane the eye's ray lands from where it entered the cornea, in metres,
|
|
40
|
+
* into `out`: `toEye` and `normal` at the cornea, `axis` the eye's, all unit vectors; `height` the
|
|
41
|
+
* cornea's height above the iris plane there.
|
|
42
|
+
*/
|
|
43
|
+
export function eyeRefractionShift(
|
|
44
|
+
toEye: Vec3,
|
|
45
|
+
normal: Vec3,
|
|
46
|
+
axis: Vec3,
|
|
47
|
+
ior: number,
|
|
48
|
+
height: number,
|
|
49
|
+
out: Float64Array,
|
|
50
|
+
): void {
|
|
51
|
+
const ray = new Float64Array(3);
|
|
52
|
+
refract([-toEye[0], -toEye[1], -toEye[2]], normal, 1 / ior, ray);
|
|
53
|
+
const descent = Math.max(
|
|
54
|
+
-((ray[0] as number) * axis[0] + (ray[1] as number) * axis[1] + (ray[2] as number) * axis[2]),
|
|
55
|
+
EYE_MIN_DESCENT,
|
|
56
|
+
);
|
|
57
|
+
const reach = height / descent;
|
|
58
|
+
const along =
|
|
59
|
+
((ray[0] as number) * axis[0] + (ray[1] as number) * axis[1] + (ray[2] as number) * axis[2]) *
|
|
60
|
+
reach;
|
|
61
|
+
for (let c = 0; c < 3; c++) out[c] = (ray[c] as number) * reach - axis[c] * along;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Where the iris is, without a map: 1 inside `irisRadius` of the texture's centre, 0 outside, across
|
|
66
|
+
* an edge `edge` wide — a limbus, not a cut.
|
|
67
|
+
*/
|
|
68
|
+
export function irisMask(u: number, v: number, irisRadius: number, edge: number): number {
|
|
69
|
+
const r = Math.hypot(u - 0.5, v - 0.5);
|
|
70
|
+
const t = Math.min(Math.max((r - (irisRadius - edge)) / (2 * edge), 0), 1);
|
|
71
|
+
return 1 - t * t * (3 - 2 * t);
|
|
72
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BC on WebGL2: which formats this context samples, and the upload of a stored chain.
|
|
3
|
+
*
|
|
4
|
+
* **Four extensions, each a different subset**, and the names a device offers are read off them:
|
|
5
|
+
* `WEBGL_compressed_texture_s3tc` is BC1 to BC3, its `_srgb` twin their sRGB forms,
|
|
6
|
+
* `EXT_texture_compression_rgtc` BC4 and BC5, and `EXT_texture_compression_bptc` BC7 in both. A
|
|
7
|
+
* desktop GPU usually has all four and a phone none, which is why the loader decodes there.
|
|
8
|
+
*
|
|
9
|
+
* BC1 is uploaded as `COMPRESSED_RGBA_S3TC_DXT1_EXT` rather than the RGB one, so a three-colour
|
|
10
|
+
* block's fourth index is transparent here as it is on WebGPU, whose only BC1 is `bc1-rgba-unorm`.
|
|
11
|
+
*
|
|
12
|
+
* The stored levels are uploaded as they are and no chain is generated: `generateMipmap` cannot
|
|
13
|
+
* write a compressed format, and the chain its author stored is the better one anyway. A chain that
|
|
14
|
+
* stops before 1x1 sets `TEXTURE_MAX_LEVEL` to its last level, or the texture is incomplete and
|
|
15
|
+
* samples black.
|
|
16
|
+
*/
|
|
17
|
+
import type { CompressedTextureFormat, CompressedTextureSource } from './compressedSource.ts';
|
|
18
|
+
|
|
19
|
+
/** The constants each extension defines, written out so a missing extension cannot be read for one. */
|
|
20
|
+
const INTERNAL: Readonly<Record<CompressedTextureFormat, number>> = {
|
|
21
|
+
bc1: 0x83f1, // COMPRESSED_RGBA_S3TC_DXT1_EXT
|
|
22
|
+
bc2: 0x83f2, // COMPRESSED_RGBA_S3TC_DXT3_EXT
|
|
23
|
+
bc3: 0x83f3, // COMPRESSED_RGBA_S3TC_DXT5_EXT
|
|
24
|
+
'bc1-srgb': 0x8c4d, // COMPRESSED_SRGB_ALPHA_S3TC_DXT1_EXT
|
|
25
|
+
'bc2-srgb': 0x8c4e, // COMPRESSED_SRGB_ALPHA_S3TC_DXT3_EXT
|
|
26
|
+
'bc3-srgb': 0x8c4f, // COMPRESSED_SRGB_ALPHA_S3TC_DXT5_EXT
|
|
27
|
+
bc4: 0x8dbb, // COMPRESSED_RED_RGTC1_EXT
|
|
28
|
+
bc5: 0x8dbd, // COMPRESSED_RED_GREEN_RGTC2_EXT
|
|
29
|
+
bc7: 0x8e8c, // COMPRESSED_RGBA_BPTC_UNORM_EXT
|
|
30
|
+
'bc7-srgb': 0x8e8d, // COMPRESSED_SRGB_ALPHA_BPTC_UNORM_EXT
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const BY_EXTENSION: readonly (readonly [string, readonly CompressedTextureFormat[]])[] = [
|
|
34
|
+
['WEBGL_compressed_texture_s3tc', ['bc1', 'bc2', 'bc3']],
|
|
35
|
+
['WEBGL_compressed_texture_s3tc_srgb', ['bc1-srgb', 'bc2-srgb', 'bc3-srgb']],
|
|
36
|
+
['EXT_texture_compression_rgtc', ['bc4', 'bc5']],
|
|
37
|
+
['EXT_texture_compression_bptc', ['bc7', 'bc7-srgb']],
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The BC formats this context samples, enabling each extension that offers some. Enabling is what
|
|
42
|
+
* `getExtension` does, and a format is only legal in an upload once it has been called.
|
|
43
|
+
*/
|
|
44
|
+
export function glCompressedFormats(gl: WebGL2RenderingContext): CompressedTextureFormat[] {
|
|
45
|
+
const out: CompressedTextureFormat[] = [];
|
|
46
|
+
for (const [name, formats] of BY_EXTENSION) {
|
|
47
|
+
if (gl.getExtension(name) !== null) out.push(...formats);
|
|
48
|
+
}
|
|
49
|
+
return out;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Upload `layers`, every one of `levels` levels, to the bound `TEXTURE_2D_ARRAY` as `name`, and cap
|
|
54
|
+
* the chain at what was stored. The caller has checked the sources and the device.
|
|
55
|
+
*/
|
|
56
|
+
export function uploadCompressedArray(
|
|
57
|
+
gl: WebGL2RenderingContext,
|
|
58
|
+
name: CompressedTextureFormat,
|
|
59
|
+
layers: readonly CompressedTextureSource[],
|
|
60
|
+
levels: number,
|
|
61
|
+
): void {
|
|
62
|
+
const first = layers[0] as CompressedTextureSource;
|
|
63
|
+
for (let level = 0; level < levels; level++) {
|
|
64
|
+
let size = 0;
|
|
65
|
+
for (const layer of layers) size += (layer.levels[level] as Uint8Array).length;
|
|
66
|
+
const data = new Uint8Array(size);
|
|
67
|
+
let at = 0;
|
|
68
|
+
for (const layer of layers) {
|
|
69
|
+
const bytes = layer.levels[level] as Uint8Array;
|
|
70
|
+
data.set(bytes, at);
|
|
71
|
+
at += bytes.length;
|
|
72
|
+
}
|
|
73
|
+
gl.compressedTexImage3D(
|
|
74
|
+
gl.TEXTURE_2D_ARRAY,
|
|
75
|
+
level,
|
|
76
|
+
INTERNAL[name],
|
|
77
|
+
Math.max(1, first.width >> level),
|
|
78
|
+
Math.max(1, first.height >> level),
|
|
79
|
+
layers.length,
|
|
80
|
+
0,
|
|
81
|
+
data,
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_BASE_LEVEL, 0);
|
|
85
|
+
gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_MAX_LEVEL, levels - 1);
|
|
86
|
+
}
|
|
@@ -7,6 +7,7 @@ import { DEPTH_01_TO_CLIP } from './lightVolumeDraw.ts';
|
|
|
7
7
|
import { compileProgram, uniformLocations } from './shader.ts';
|
|
8
8
|
import { FULLSCREEN_VERT } from './shaders/fullscreen.ts';
|
|
9
9
|
import { MEDIUM_FRAG, MEDIUM_UPSAMPLE_FRAG } from './shaders/globalMedium.ts';
|
|
10
|
+
import { blendCovering, blendTransmitting } from './sceneCoverage.ts';
|
|
10
11
|
|
|
11
12
|
/** What the march needs about the light, which is the sun and the sky and nothing else. */
|
|
12
13
|
export interface MediumLight {
|
|
@@ -270,7 +271,7 @@ export class GlobalMediumPass {
|
|
|
270
271
|
gl.depthMask(false);
|
|
271
272
|
gl.disable(gl.CULL_FACE);
|
|
272
273
|
gl.enable(gl.BLEND);
|
|
273
|
-
|
|
274
|
+
blendTransmitting(gl, gl.ONE, gl.SRC_ALPHA);
|
|
274
275
|
|
|
275
276
|
gl.useProgram(this.upsample);
|
|
276
277
|
gl.bindVertexArray(this.vao);
|
|
@@ -293,7 +294,7 @@ export class GlobalMediumPass {
|
|
|
293
294
|
gl.activeTexture(gl.TEXTURE0);
|
|
294
295
|
gl.bindTexture(gl.TEXTURE_2D, null);
|
|
295
296
|
/* Back to the blend every other pass in this renderer assumes it inherits. */
|
|
296
|
-
|
|
297
|
+
blendCovering(gl, gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
|
|
297
298
|
gl.disable(gl.BLEND);
|
|
298
299
|
gl.depthMask(true);
|
|
299
300
|
gl.enable(gl.DEPTH_TEST);
|