@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,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A skinned cloth for a renderer: `@driftengine/physics`' solver, run as compute on WebGPU — the
|
|
3
|
+
* particles never leave the device — and as `SkinnedCloth` on the CPU under WebGL2, which has no
|
|
4
|
+
* compute. One schedule, `ClothControl`, and the same kernels either way; what a frame draws is
|
|
5
|
+
* `particles`, through `setCloth(binding, cloth.particles)`.
|
|
6
|
+
*
|
|
7
|
+
* **A function rather than renderer methods, so a game pays for it only by importing it.** See
|
|
8
|
+
* `backend/webgpu/computeHost.ts` for the door it reaches the device through, and why.
|
|
9
|
+
*
|
|
10
|
+
* **The CPU fallback's cost**, measured on a desktop part: a few hundred particles are a
|
|
11
|
+
* millisecond a frame on the main thread, and 4,500 are twenty — a garment that size wants the
|
|
12
|
+
* device. The device solver ran 4,500 particles in 0.65 ms a frame, most of it the dispatches
|
|
13
|
+
* rather than the particles (`demo/dev/skinnedCloth.html?bench=1`).
|
|
14
|
+
*/
|
|
15
|
+
import { SkinnedCloth } from '@driftengine/physics';
|
|
16
|
+
import type { SkinnedClothSetup } from '@driftengine/physics';
|
|
17
|
+
|
|
18
|
+
import type { ClothParticlesHandle, RendererApi } from './backend/api.ts';
|
|
19
|
+
import { computeHostOf } from './backend/webgpu/computeHost.ts';
|
|
20
|
+
import type { ComputeHost } from './backend/webgpu/computeHost.ts';
|
|
21
|
+
import { GpuSkinnedCloth } from './backend/webgpu/gpuSkinnedCloth.ts';
|
|
22
|
+
|
|
23
|
+
/** A garment a renderer solves, from `createSkinnedCloth`. */
|
|
24
|
+
export interface SkinnedClothSolver {
|
|
25
|
+
/** What `setCloth(binding, particles)` draws the garment's bound meshes by. */
|
|
26
|
+
readonly particles: ClothParticlesHandle;
|
|
27
|
+
/** Where the solver runs: on the device, or on the CPU beneath a backend without compute. */
|
|
28
|
+
readonly runsOn: 'device' | 'cpu';
|
|
29
|
+
/**
|
|
30
|
+
* One frame: the rig's pose — each joint's global matrix, model space, sixteen floats a joint,
|
|
31
|
+
* and the model matrix — and `dt` seconds, run as whole fixed steps; the particles at the frame's
|
|
32
|
+
* `alpha` are then what the frame draws. **Once a frame, before the draws**, as
|
|
33
|
+
* `updateClothParticles` is: a second in one frame would reach every draw of it on WebGPU.
|
|
34
|
+
*/
|
|
35
|
+
step(globals: Float32Array, model: Float32Array, dt: number): void;
|
|
36
|
+
/**
|
|
37
|
+
* The air the cloth drags toward from the next step, world space, metres a second: the frame's
|
|
38
|
+
* sample of the scene's one `WindField`, so a garment moves with everything else in the air.
|
|
39
|
+
*/
|
|
40
|
+
setWind(x: number, y: number, z: number): void;
|
|
41
|
+
/** Back to the skinned pose, settled, as a teleport resets it. */
|
|
42
|
+
reset(): void;
|
|
43
|
+
dispose(): void;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** A garment for `renderer`, from a set-up checked by name. */
|
|
47
|
+
export function createSkinnedCloth(
|
|
48
|
+
renderer: RendererApi,
|
|
49
|
+
setup: SkinnedClothSetup,
|
|
50
|
+
): SkinnedClothSolver {
|
|
51
|
+
const host = computeHostOf(renderer);
|
|
52
|
+
return host === null ? new CpuCloth(renderer, setup) : new DeviceCloth(host, setup);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
class DeviceCloth implements SkinnedClothSolver {
|
|
56
|
+
readonly runsOn = 'device';
|
|
57
|
+
readonly particles: ClothParticlesHandle;
|
|
58
|
+
private readonly cloth: GpuSkinnedCloth;
|
|
59
|
+
|
|
60
|
+
constructor(
|
|
61
|
+
private readonly host: ComputeHost,
|
|
62
|
+
setup: SkinnedClothSetup,
|
|
63
|
+
) {
|
|
64
|
+
this.cloth = new GpuSkinnedCloth(host.device(), setup);
|
|
65
|
+
this.particles = this.cloth.particles;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
step(globals: Float32Array, model: Float32Array, dt: number): void {
|
|
69
|
+
if (this.host.lost()) return;
|
|
70
|
+
this.cloth.advance(globals, model, dt, this.host.changeFrame());
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
setWind(x: number, y: number, z: number): void {
|
|
74
|
+
this.cloth.setWind(x, y, z);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
reset(): void {
|
|
78
|
+
if (this.host.lost()) return;
|
|
79
|
+
this.cloth.reset();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Its buffers at once — its work is submitted — and its textures with the frame's. */
|
|
83
|
+
dispose(): void {
|
|
84
|
+
this.cloth.dispose();
|
|
85
|
+
this.host.retire(this.cloth.particles.textures);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** The particles where the last step left them: the parity check's door, waits on the device. */
|
|
89
|
+
read(): Promise<Float32Array> {
|
|
90
|
+
return this.cloth.read();
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
class CpuCloth implements SkinnedClothSolver {
|
|
95
|
+
readonly runsOn = 'cpu';
|
|
96
|
+
readonly particles: ClothParticlesHandle;
|
|
97
|
+
private readonly cloth: SkinnedCloth;
|
|
98
|
+
/** The frame's particles at `alpha`, before they go up. */
|
|
99
|
+
private readonly staging: Float32Array;
|
|
100
|
+
|
|
101
|
+
constructor(
|
|
102
|
+
private readonly renderer: RendererApi,
|
|
103
|
+
setup: SkinnedClothSetup,
|
|
104
|
+
) {
|
|
105
|
+
this.cloth = new SkinnedCloth(setup);
|
|
106
|
+
this.particles = renderer.createClothParticles(this.cloth.count);
|
|
107
|
+
this.staging = new Float32Array(this.cloth.count * 3);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
step(globals: Float32Array, model: Float32Array, dt: number): void {
|
|
111
|
+
this.cloth.setPose(globals, model);
|
|
112
|
+
this.cloth.advance(dt);
|
|
113
|
+
this.cloth.interpolate(this.cloth.alpha, this.staging);
|
|
114
|
+
this.renderer.updateClothParticles(this.particles, this.staging);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
setWind(x: number, y: number, z: number): void {
|
|
118
|
+
this.cloth.setWind(x, y, z);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
reset(): void {
|
|
122
|
+
this.cloth.reset();
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
dispose(): void {
|
|
126
|
+
this.renderer.disposeClothParticles(this.particles);
|
|
127
|
+
}
|
|
128
|
+
}
|
package/src/render/ssrPass.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { compileProgram, uniformLocations } from './shader.ts';
|
|
|
8
8
|
import { FULLSCREEN_VERT } from './shaders/fullscreen.ts';
|
|
9
9
|
import { SSR_RESOLVE_FRAG } from './shaders/ssrResolve.ts';
|
|
10
10
|
import { SSR_TRACE_FRAG } from './shaders/ssrTrace.ts';
|
|
11
|
+
import { blendKeeping } from './sceneCoverage.ts';
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* Screen-space reflection on WebGL2: a scissored trace per surface, then one composite.
|
|
@@ -202,7 +203,7 @@ export class SsrPass {
|
|
|
202
203
|
gl.disable(gl.CULL_FACE);
|
|
203
204
|
gl.enable(gl.BLEND);
|
|
204
205
|
/* Premultiplied `over`: the trace already multiplied its colour by the coverage it found. */
|
|
205
|
-
|
|
206
|
+
blendKeeping(gl, gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
|
|
206
207
|
|
|
207
208
|
gl.useProgram(this.resolve);
|
|
208
209
|
gl.activeTexture(gl.TEXTURE0);
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A material's shading model: how its surface answers light, when the standard model is not what
|
|
3
|
+
* it is made of — brushed metal, hair, skin, an eye.
|
|
4
|
+
*
|
|
5
|
+
* **A frozen descriptor, made once by a factory, carrying a kind and that model's numbers.** The
|
|
6
|
+
* standard model is `null`, exactly today's shader, and a scene that never names a model compiles
|
|
7
|
+
* nothing of any of them: each is a pipeline constant of its own (`MODEL_ANISOTROPIC` and the rest
|
|
8
|
+
* in `shaders/flat/models.ts`), off in every pipeline but the ones a material asks for.
|
|
9
|
+
*
|
|
10
|
+
* **Two vectors of numbers a material, and no more.** The lit stage's eight-light build is 254 of
|
|
11
|
+
* the 256 uniform vectors an Adreno 740 offers, so a model's numbers are packed into
|
|
12
|
+
* `uModelParams[2]` — the last component of the second says whether a model map is bound — and
|
|
13
|
+
* every model fits. See `packModel` for each layout.
|
|
14
|
+
*
|
|
15
|
+
* **The eye's axis is the one number that moves.** It is the eye's forward direction in the world,
|
|
16
|
+
* which a turning head or a glance changes every frame; the lit stage has no room left for another
|
|
17
|
+
* varying (sixteen, WebGPU's ceiling), so the renderer turns the descriptor's mesh-local axis by
|
|
18
|
+
* the draw's model matrix and the eye's joint each draw, with `eyeAxisInWorld`.
|
|
19
|
+
*/
|
|
20
|
+
import type { Vec3 } from '../math/color.ts';
|
|
21
|
+
|
|
22
|
+
/** Strands of brushed metal, satin or hair-like fibre: a highlight stretched across them. */
|
|
23
|
+
export interface AnisotropicModel {
|
|
24
|
+
readonly kind: 'anisotropic';
|
|
25
|
+
/** 0 to 1: how far the highlight is stretched along the tangent. */
|
|
26
|
+
readonly strength: number;
|
|
27
|
+
/** Radians, turning the stretch's direction from the tangent toward the bitangent. */
|
|
28
|
+
readonly rotation: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Hair: three lobes along the strand, which is the mesh's tangent, running from root to tip
|
|
33
|
+
* (`hairLobes.ts`). A model map's green turns each strand's tilt by up to half of it either way and
|
|
34
|
+
* its blue is occlusion.
|
|
35
|
+
*/
|
|
36
|
+
export interface HairModel {
|
|
37
|
+
readonly kind: 'hair';
|
|
38
|
+
/**
|
|
39
|
+
* Radians the cuticle's scales tilt the strand's surface: positive moves the white highlight
|
|
40
|
+
* toward the root and the coloured one toward the tip. A card whose tangent runs tip to root
|
|
41
|
+
* takes it negative.
|
|
42
|
+
*/
|
|
43
|
+
readonly shift: number;
|
|
44
|
+
/** 0 to 1: the light scattered through many strands, tinted by their colour. */
|
|
45
|
+
readonly scatter: number;
|
|
46
|
+
/** How strongly light from behind glows through: the transmitted lobe. */
|
|
47
|
+
readonly backlit: number;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Skin: light scattered beneath the surface, reddening its terminator and its thin parts. */
|
|
51
|
+
export interface SkinModel {
|
|
52
|
+
readonly kind: 'skin';
|
|
53
|
+
/** The colour light keeps travelling beneath the surface — the mean free path, as a colour. */
|
|
54
|
+
readonly scatterColor: Vec3;
|
|
55
|
+
/** Metres: how far light travels beneath it before it leaves. */
|
|
56
|
+
readonly radius: number;
|
|
57
|
+
/** 0 to 1: how much a light behind a thin part shows through it. */
|
|
58
|
+
readonly transmission: number;
|
|
59
|
+
/** Which of up to eight profiles the screen-space scattering blurs this skin by. */
|
|
60
|
+
readonly profile: number;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** An eye: an iris seen through a cornea, which refracts it, under a mirror-smooth highlight. */
|
|
64
|
+
export interface EyeModel {
|
|
65
|
+
readonly kind: 'eye';
|
|
66
|
+
/** The iris's radius, in texture coordinates from the centre of the eye's map. */
|
|
67
|
+
readonly irisRadius: number;
|
|
68
|
+
/** Metres from the cornea down to the iris. */
|
|
69
|
+
readonly irisDepth: number;
|
|
70
|
+
/** The cornea's index of refraction. */
|
|
71
|
+
readonly ior: number;
|
|
72
|
+
/** How smooth the cornea's highlight is. */
|
|
73
|
+
readonly corneaRoughness: number;
|
|
74
|
+
/** The eye's forward direction, in the mesh's own space at its bind pose. */
|
|
75
|
+
readonly axis: Vec3;
|
|
76
|
+
/** The joint the eye turns with, or -1 for an eye placed by its model matrix alone. */
|
|
77
|
+
readonly joint: number;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Every model a material can name, by its `kind`: see `SurfaceMaterial.model`. */
|
|
81
|
+
export type SurfaceModel = AnisotropicModel | HairModel | SkinModel | EyeModel;
|
|
82
|
+
/** A model's name, which also picks its pipeline switch: `SURFACE_MODEL_SWITCH`. */
|
|
83
|
+
export type SurfaceModelKind = SurfaceModel['kind'];
|
|
84
|
+
|
|
85
|
+
/** Every kind, in the order the lit stage declares their switches. */
|
|
86
|
+
export const SURFACE_MODEL_KINDS: readonly SurfaceModelKind[] = [
|
|
87
|
+
'anisotropic',
|
|
88
|
+
'hair',
|
|
89
|
+
'skin',
|
|
90
|
+
'eye',
|
|
91
|
+
];
|
|
92
|
+
|
|
93
|
+
/** The lit stage's switch for each kind. */
|
|
94
|
+
export const SURFACE_MODEL_SWITCH: Readonly<Record<SurfaceModelKind, string>> = {
|
|
95
|
+
anisotropic: 'MODEL_ANISOTROPIC',
|
|
96
|
+
hair: 'MODEL_HAIR',
|
|
97
|
+
skin: 'MODEL_SKIN',
|
|
98
|
+
eye: 'MODEL_EYE',
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
/** Floats a material's model takes: `uModelParams[2]`. */
|
|
102
|
+
export const MODEL_PARAM_FLOATS = 8;
|
|
103
|
+
|
|
104
|
+
function check(model: string, name: string, value: number, low: number, high: number): number {
|
|
105
|
+
if (!Number.isFinite(value) || value < low || value > high) {
|
|
106
|
+
throw new Error(`${model}: ${name} must be from ${low} to ${high}, not ${String(value)}`);
|
|
107
|
+
}
|
|
108
|
+
return value;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function unit(model: string, name: string, v: Vec3): Vec3 {
|
|
112
|
+
const length = Math.hypot(v[0], v[1], v[2]);
|
|
113
|
+
if (!(length > 1e-6))
|
|
114
|
+
throw new Error(`${model}: ${name} must be a direction, not [${v.join(', ')}]`);
|
|
115
|
+
return [v[0] / length, v[1] / length, v[2] / length];
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Brushed metal and the like, after glTF's `KHR_materials_anisotropy`: a strength and a turn. */
|
|
119
|
+
export function anisotropicModel(
|
|
120
|
+
options: { strength?: number; rotation?: number } = {},
|
|
121
|
+
): AnisotropicModel {
|
|
122
|
+
return Object.freeze({
|
|
123
|
+
kind: 'anisotropic',
|
|
124
|
+
strength: check('anisotropicModel', 'strength', options.strength ?? 1, 0, 1),
|
|
125
|
+
rotation: check(
|
|
126
|
+
'anisotropicModel',
|
|
127
|
+
'rotation',
|
|
128
|
+
options.rotation ?? 0,
|
|
129
|
+
-2 * Math.PI,
|
|
130
|
+
2 * Math.PI,
|
|
131
|
+
),
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Hair, along the mesh's tangent from root to tip. A tilt of 3°, within the 2.5° to 5° measured. */
|
|
136
|
+
export function hairModel(
|
|
137
|
+
options: { shift?: number; scatter?: number; backlit?: number } = {},
|
|
138
|
+
): HairModel {
|
|
139
|
+
return Object.freeze({
|
|
140
|
+
kind: 'hair',
|
|
141
|
+
shift: check('hairModel', 'shift', options.shift ?? (3 * Math.PI) / 180, -0.5, 0.5),
|
|
142
|
+
scatter: check('hairModel', 'scatter', options.scatter ?? 0.7, 0, 1),
|
|
143
|
+
backlit: check('hairModel', 'backlit', options.backlit ?? 1, 0, 4),
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Skin. The defaults are a fair skin's: a red mean free path a little over a centimetre long. */
|
|
148
|
+
export function skinModel(
|
|
149
|
+
options: { scatterColor?: Vec3; radius?: number; transmission?: number; profile?: number } = {},
|
|
150
|
+
): SkinModel {
|
|
151
|
+
const color = options.scatterColor ?? [0.85, 0.35, 0.22];
|
|
152
|
+
for (let c = 0; c < 3; c++) check('skinModel', 'scatterColor', color[c] as number, 0, 1);
|
|
153
|
+
const profile = check('skinModel', 'profile', options.profile ?? 0, 0, 7);
|
|
154
|
+
if (!Number.isInteger(profile))
|
|
155
|
+
throw new Error(`skinModel: profile must be a whole number, not ${profile}`);
|
|
156
|
+
return Object.freeze({
|
|
157
|
+
kind: 'skin',
|
|
158
|
+
scatterColor: Object.freeze([color[0], color[1], color[2]]) as unknown as Vec3,
|
|
159
|
+
radius: check('skinModel', 'radius', options.radius ?? 0.012, 0, 0.1),
|
|
160
|
+
transmission: check('skinModel', 'transmission', options.transmission ?? 0.5, 0, 1),
|
|
161
|
+
profile,
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** An eye. The defaults are a human eye's, its axis along the mesh's +Z. */
|
|
166
|
+
export function eyeModel(
|
|
167
|
+
options: {
|
|
168
|
+
irisRadius?: number;
|
|
169
|
+
irisDepth?: number;
|
|
170
|
+
ior?: number;
|
|
171
|
+
corneaRoughness?: number;
|
|
172
|
+
axis?: Vec3;
|
|
173
|
+
joint?: number;
|
|
174
|
+
} = {},
|
|
175
|
+
): EyeModel {
|
|
176
|
+
const joint = options.joint ?? -1;
|
|
177
|
+
if (!Number.isInteger(joint) || joint < -1) {
|
|
178
|
+
throw new Error(`eyeModel: joint must be a joint index or -1, not ${String(joint)}`);
|
|
179
|
+
}
|
|
180
|
+
return Object.freeze({
|
|
181
|
+
kind: 'eye',
|
|
182
|
+
irisRadius: check('eyeModel', 'irisRadius', options.irisRadius ?? 0.15, 0.001, 0.5),
|
|
183
|
+
irisDepth: check('eyeModel', 'irisDepth', options.irisDepth ?? 0.003, 0, 0.02),
|
|
184
|
+
ior: check('eyeModel', 'ior', options.ior ?? 1.336, 1, 2),
|
|
185
|
+
corneaRoughness: check('eyeModel', 'corneaRoughness', options.corneaRoughness ?? 0.02, 0, 1),
|
|
186
|
+
axis: Object.freeze(unit('eyeModel', 'axis', options.axis ?? [0, 0, 1])) as unknown as Vec3,
|
|
187
|
+
joint,
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* A model's numbers into `out` — eight floats, `uModelParams[0]` then `[1]` — and whether a model
|
|
193
|
+
* map is bound into the last. Zeros for the standard model. An eye's axis goes in with the
|
|
194
|
+
* descriptor's own until the draw turns it: see `eyeAxisInWorld`.
|
|
195
|
+
*
|
|
196
|
+
* - anisotropic: strength, cos and sin of the rotation · −, −, −, map
|
|
197
|
+
* - hair: shift, scatter, backlit, − · −, −, −, map
|
|
198
|
+
* - skin: the scatter colour, radius · transmission, profile, −, map
|
|
199
|
+
* - eye: iris radius, iris depth, ior, cornea roughness · the axis, map
|
|
200
|
+
*/
|
|
201
|
+
export function packModel(model: SurfaceModel | null, mapped: boolean, out: Float32Array): void {
|
|
202
|
+
out.fill(0);
|
|
203
|
+
if (model === null) return;
|
|
204
|
+
out[7] = mapped ? 1 : 0;
|
|
205
|
+
switch (model.kind) {
|
|
206
|
+
case 'anisotropic':
|
|
207
|
+
out[0] = model.strength;
|
|
208
|
+
out[1] = Math.cos(model.rotation);
|
|
209
|
+
out[2] = Math.sin(model.rotation);
|
|
210
|
+
return;
|
|
211
|
+
case 'hair':
|
|
212
|
+
out[0] = model.shift;
|
|
213
|
+
out[1] = model.scatter;
|
|
214
|
+
out[2] = model.backlit;
|
|
215
|
+
return;
|
|
216
|
+
case 'skin':
|
|
217
|
+
out[0] = model.scatterColor[0];
|
|
218
|
+
out[1] = model.scatterColor[1];
|
|
219
|
+
out[2] = model.scatterColor[2];
|
|
220
|
+
out[3] = model.radius;
|
|
221
|
+
out[4] = model.transmission;
|
|
222
|
+
out[5] = model.profile;
|
|
223
|
+
return;
|
|
224
|
+
case 'eye':
|
|
225
|
+
out[0] = model.irisRadius;
|
|
226
|
+
out[1] = model.irisDepth;
|
|
227
|
+
out[2] = model.ior;
|
|
228
|
+
out[3] = model.corneaRoughness;
|
|
229
|
+
out[4] = model.axis[0];
|
|
230
|
+
out[5] = model.axis[1];
|
|
231
|
+
out[6] = model.axis[2];
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* An eye's axis in the world for one draw, into `out[at…at+2]`: the descriptor's mesh-local axis
|
|
238
|
+
* turned by its joint's palette entry, where it names one and a palette is set, and then by the
|
|
239
|
+
* draw's model matrix — the turn its vertices take, without the translation. Normalised, because
|
|
240
|
+
* a palette carries a joint's scale.
|
|
241
|
+
*/
|
|
242
|
+
export function eyeAxisInWorld(
|
|
243
|
+
model: EyeModel,
|
|
244
|
+
placement: ArrayLike<number>,
|
|
245
|
+
palette: ArrayLike<number> | null,
|
|
246
|
+
out: Float32Array,
|
|
247
|
+
at: number,
|
|
248
|
+
): void {
|
|
249
|
+
let [x, y, z] = model.axis;
|
|
250
|
+
const j = model.joint * 16;
|
|
251
|
+
if (palette !== null && model.joint >= 0 && j + 16 <= palette.length) {
|
|
252
|
+
const px =
|
|
253
|
+
(palette[j] as number) * x + (palette[j + 4] as number) * y + (palette[j + 8] as number) * z;
|
|
254
|
+
const py =
|
|
255
|
+
(palette[j + 1] as number) * x +
|
|
256
|
+
(palette[j + 5] as number) * y +
|
|
257
|
+
(palette[j + 9] as number) * z;
|
|
258
|
+
const pz =
|
|
259
|
+
(palette[j + 2] as number) * x +
|
|
260
|
+
(palette[j + 6] as number) * y +
|
|
261
|
+
(palette[j + 10] as number) * z;
|
|
262
|
+
x = px;
|
|
263
|
+
y = py;
|
|
264
|
+
z = pz;
|
|
265
|
+
}
|
|
266
|
+
const m = placement;
|
|
267
|
+
const wx = (m[0] as number) * x + (m[4] as number) * y + (m[8] as number) * z;
|
|
268
|
+
const wy = (m[1] as number) * x + (m[5] as number) * y + (m[9] as number) * z;
|
|
269
|
+
const wz = (m[2] as number) * x + (m[6] as number) * y + (m[10] as number) * z;
|
|
270
|
+
const length = Math.hypot(wx, wy, wz) || 1;
|
|
271
|
+
out[at] = wx / length;
|
|
272
|
+
out[at + 1] = wy / length;
|
|
273
|
+
out[at + 2] = wz / length;
|
|
274
|
+
}
|
|
@@ -8,9 +8,19 @@
|
|
|
8
8
|
* array samples exactly as a 2D texture does. A mesh names its layer per vertex (`MeshData.layers`),
|
|
9
9
|
* so a merged block wearing forty facades is one draw.
|
|
10
10
|
*/
|
|
11
|
-
import {
|
|
11
|
+
import type { SurfaceModel } from './surfaceModel.ts';
|
|
12
|
+
import { layerSize, refuseArrayUpdate, sourceSize } from './textureSource.ts';
|
|
13
|
+
import { compressedLayers, planBlocks, refuseBlockUpdate } from './compressedSource.ts';
|
|
14
|
+
import type {
|
|
15
|
+
BlockFormat,
|
|
16
|
+
CompressedTextureFormat,
|
|
17
|
+
CompressedTextureSource,
|
|
18
|
+
SurfaceSource,
|
|
19
|
+
} from './compressedSource.ts';
|
|
20
|
+
import { uploadCompressedArray } from './glCompressed.ts';
|
|
12
21
|
import { SURFACE_EFFECT_TEXELS, packSurfaceEffects } from './surfaceEffects.ts';
|
|
13
22
|
import type { SurfaceLayerEffect } from './surfaceEffects.ts';
|
|
23
|
+
import type { CutoutMode } from './cutoutDither.ts';
|
|
14
24
|
|
|
15
25
|
/**
|
|
16
26
|
* How a texture behaves past its edges and between its texels.
|
|
@@ -70,8 +80,31 @@ export interface SurfaceMaterial<Texture = SurfaceTexture> {
|
|
|
70
80
|
/** Repeats across the mesh's own UV range, per axis. Applies to every map the material holds. */
|
|
71
81
|
uScale?: number;
|
|
72
82
|
vScale?: number;
|
|
73
|
-
/**
|
|
83
|
+
/**
|
|
84
|
+
* Where the texture starts, per axis, added after the scale: a surface samples
|
|
85
|
+
* `uv · scale + offset`. Applies to every map, and to a cutout's shadow and depth as well.
|
|
86
|
+
*
|
|
87
|
+
* **What picks a cell of a flipbook or an atlas through the material**, so one quad draws any
|
|
88
|
+
* cell: a strip of four frames is `uScale: 0.25` and `uOffset: frame * 0.25`. Without it a
|
|
89
|
+
* consumer built one quad mesh per cell — 106 meshes for four strips — because the only way to
|
|
90
|
+
* move a texture was to move the geometry's own coordinates.
|
|
91
|
+
*/
|
|
92
|
+
uOffset?: number;
|
|
93
|
+
vOffset?: number;
|
|
94
|
+
/** Alpha below which a fragment is discarded. See `uCutout`. */
|
|
74
95
|
cutout?: number;
|
|
96
|
+
/**
|
|
97
|
+
* How the cutout's edge is drawn: `'hard'`, the default and every cutout before 4.8.4, or
|
|
98
|
+
* `'dithered'`, which keeps a pixel by the share of it the texture covers so a strand of hair, a
|
|
99
|
+
* lash or a fringe of leaves has a soft edge instead of a stair of pixels.
|
|
100
|
+
*
|
|
101
|
+
* **The frame decides how a dithered edge is resolved**, because the material cannot know: under a
|
|
102
|
+
* temporal resolve (TAA or DriftTR) the pattern moves every frame and the resolve averages it;
|
|
103
|
+
* multisampled, the share goes to the GPU as alpha-to-coverage; with neither, the test is hard,
|
|
104
|
+
* since a dither nothing averages is grain. A translucent draw tests hard whatever this says. See
|
|
105
|
+
* `cutoutDither.ts`.
|
|
106
|
+
*/
|
|
107
|
+
cutoutMode?: CutoutMode;
|
|
75
108
|
/**
|
|
76
109
|
* Whether both faces are seen: glTF's `doubleSided`, what a curtain or a leaf card is. Drawn
|
|
77
110
|
* without culling, and a back face is lit as its front, with the normal turned to the viewer.
|
|
@@ -112,6 +145,19 @@ export interface SurfaceMaterial<Texture = SurfaceTexture> {
|
|
|
112
145
|
* consumer who wants a bound map twice as bright asks here rather than rebuilding the mesh.
|
|
113
146
|
*/
|
|
114
147
|
emissiveScale?: readonly [number, number, number];
|
|
148
|
+
/**
|
|
149
|
+
* How the surface answers light, when the standard model is not what it is made of: brushed
|
|
150
|
+
* metal, hair, skin, an eye — `anisotropicModel`, `hairModel`, `skinModel`, `eyeModel`. Null or
|
|
151
|
+
* absent is the standard model, exactly as before. **A pipeline of its own**, compiled the first
|
|
152
|
+
* time a draw asks, so a scene that names no model pays for none. See `surfaceModel.ts`.
|
|
153
|
+
*/
|
|
154
|
+
model?: SurfaceModel | null;
|
|
155
|
+
/**
|
|
156
|
+
* The model's own channels, in the albedo's coordinates and layers; what each channel means is
|
|
157
|
+
* the model's to say. Absent, each model takes its own neutral. Sampled linear, like any map
|
|
158
|
+
* that is not a colour.
|
|
159
|
+
*/
|
|
160
|
+
modelMap?: Texture | null;
|
|
115
161
|
}
|
|
116
162
|
|
|
117
163
|
export interface SurfaceTextureOptions {
|
|
@@ -211,79 +257,45 @@ export class SurfaceTexture {
|
|
|
211
257
|
readonly layers: number;
|
|
212
258
|
/** The per-layer effects table, or null for a texture given none. See `surfaceEffects.ts`. */
|
|
213
259
|
private effectsTable: WebGLTexture | null = null;
|
|
260
|
+
/** The block format a compressed texture holds, or null for one uploaded from images. */
|
|
261
|
+
private readonly blockFormat: BlockFormat | null;
|
|
214
262
|
|
|
215
263
|
/**
|
|
216
264
|
* One image, or an array of images that must share one size (`layerSize` refuses otherwise).
|
|
217
|
-
* Uploaded unflipped either way; see the constructor body for why.
|
|
265
|
+
* Uploaded unflipped either way; see the constructor body for why. Or blocks, uploaded as they are
|
|
266
|
+
* where `compressed` — the formats this context samples — has theirs; see `compressedSource.ts`.
|
|
218
267
|
*/
|
|
219
268
|
constructor(
|
|
220
269
|
gl: WebGL2RenderingContext,
|
|
221
|
-
source:
|
|
270
|
+
source: SurfaceSource | readonly SurfaceSource[],
|
|
222
271
|
options: SurfaceTextureOptions = {},
|
|
272
|
+
compressed: readonly CompressedTextureFormat[] = [],
|
|
223
273
|
) {
|
|
224
|
-
const
|
|
225
|
-
const
|
|
226
|
-
this.
|
|
274
|
+
const listed = (Array.isArray(source) ? source : [source]) as readonly SurfaceSource[];
|
|
275
|
+
const blocks = compressedLayers(listed);
|
|
276
|
+
this.srgb = (options.colorSpace ?? 'linear') === 'srgb';
|
|
277
|
+
/* Refused before anything is allocated, so a refusal leaves no texture behind. */
|
|
278
|
+
const plan = blocks === null ? null : planBlocks(blocks, this.srgb, compressed);
|
|
227
279
|
const texture = gl.createTexture();
|
|
228
280
|
if (texture === null) throw new Error('SurfaceTexture: createTexture failed');
|
|
229
281
|
this.texture = texture;
|
|
230
|
-
this.mipmapped = options.mipmap ?? true;
|
|
231
|
-
this.srgb = (options.colorSpace ?? 'linear') === 'srgb';
|
|
232
|
-
|
|
233
282
|
const wrap = (options.wrap ?? 'repeat') === 'repeat' ? gl.REPEAT : gl.CLAMP_TO_EDGE;
|
|
234
|
-
|
|
235
283
|
gl.bindTexture(gl.TEXTURE_2D_ARRAY, texture);
|
|
236
|
-
/*
|
|
237
|
-
* **Not flipped, and this used to be, which is the bug it is written down for.**
|
|
238
|
-
*
|
|
239
|
-
* The flip was here with a comment saying every source is a 2D drawing surface whose Y
|
|
240
|
-
* runs down from the top-left, so without it "every texture arrives mirrored". That is
|
|
241
|
-
* true of a canvas and **WebGL ignores `UNPACK_FLIP_Y_WEBGL` entirely for an
|
|
242
|
-
* `ImageBitmap`**, which carries its own orientation. So the two source types the same
|
|
243
|
-
* method accepts arrived the opposite way up from each other, and nothing caught it
|
|
244
|
-
* because every canvas-sourced texture in this repository was symmetric under a vertical
|
|
245
|
-
* flip: eroded noise, a radial halo, a sleeve blurred to 24 pixels and back. One consumer
|
|
246
|
-
* painted lettering to a canvas beside a sleeve decoded as a bitmap and the text was
|
|
247
|
-
* upside down.
|
|
248
|
-
*
|
|
249
|
-
* **Made to agree by dropping the flip rather than by extending it**, and the direction
|
|
250
|
-
* is a deliberate choice about blast radius rather than a coin toss: `drftLoader` builds
|
|
251
|
-
* every model texture from an `ImageBitmap`, so flipping bitmaps to match canvases would
|
|
252
|
-
* turn over every painted surface on every loaded model. Dropping it changes only
|
|
253
|
-
* canvas-sourced textures, and every one of those that exists today is symmetric.
|
|
254
|
-
*
|
|
255
|
-
* A caller that was mirroring its own canvas to cancel this must stop.
|
|
256
|
-
*/
|
|
257
|
-
/* Allocated mutable rather than with texStorage3D, so `update` can hand a one-layer texture a
|
|
258
|
-
new size, as texImage2D always could. Each layer is then written in place. */
|
|
259
|
-
gl.texImage3D(
|
|
260
|
-
gl.TEXTURE_2D_ARRAY,
|
|
261
|
-
0,
|
|
262
|
-
this.srgb ? gl.SRGB8_ALPHA8 : gl.RGBA,
|
|
263
|
-
width,
|
|
264
|
-
height,
|
|
265
|
-
sources.length,
|
|
266
|
-
0,
|
|
267
|
-
gl.RGBA,
|
|
268
|
-
gl.UNSIGNED_BYTE,
|
|
269
|
-
null,
|
|
270
|
-
);
|
|
271
|
-
for (let layer = 0; layer < sources.length; layer++) {
|
|
272
|
-
gl.texSubImage3D(
|
|
273
|
-
gl.TEXTURE_2D_ARRAY,
|
|
274
|
-
0,
|
|
275
|
-
0,
|
|
276
|
-
0,
|
|
277
|
-
layer,
|
|
278
|
-
width,
|
|
279
|
-
height,
|
|
280
|
-
1,
|
|
281
|
-
gl.RGBA,
|
|
282
|
-
gl.UNSIGNED_BYTE,
|
|
283
|
-
sources[layer] as TexImageSource,
|
|
284
|
-
);
|
|
285
|
-
}
|
|
286
284
|
|
|
285
|
+
if (blocks !== null && plan !== null) {
|
|
286
|
+
/* The stored chain, or level 0 alone where no chain was asked for. Nothing is generated:
|
|
287
|
+
`generateMipmap` cannot write a compressed format. */
|
|
288
|
+
this.layers = blocks.length;
|
|
289
|
+
this.blockFormat = (blocks[0] as CompressedTextureSource).format;
|
|
290
|
+
this.mipmapped = (options.mipmap ?? true) && plan.levels > 1;
|
|
291
|
+
uploadCompressedArray(gl, plan.name, blocks, this.mipmapped ? plan.levels : 1);
|
|
292
|
+
} else {
|
|
293
|
+
const sources = listed as readonly TexImageSource[];
|
|
294
|
+
this.layers = sources.length;
|
|
295
|
+
this.blockFormat = null;
|
|
296
|
+
this.mipmapped = options.mipmap ?? true;
|
|
297
|
+
uploadImages(gl, sources, this.srgb);
|
|
298
|
+
}
|
|
287
299
|
gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_WRAP_S, wrap);
|
|
288
300
|
gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_WRAP_T, wrap);
|
|
289
301
|
/* Nearest where the caller said their pixels are the subject; see `filter`. The mip chain is
|
|
@@ -303,7 +315,7 @@ export class SurfaceTexture {
|
|
|
303
315
|
: gl.LINEAR,
|
|
304
316
|
);
|
|
305
317
|
|
|
306
|
-
if (this.mipmapped) gl.generateMipmap(gl.TEXTURE_2D_ARRAY);
|
|
318
|
+
if (this.mipmapped && this.blockFormat === null) gl.generateMipmap(gl.TEXTURE_2D_ARRAY);
|
|
307
319
|
applyAnisotropy(gl, options.anisotropy ?? DEFAULT_ANISOTROPY);
|
|
308
320
|
gl.bindTexture(gl.TEXTURE_2D_ARRAY, null);
|
|
309
321
|
|
|
@@ -334,7 +346,6 @@ export class SurfaceTexture {
|
|
|
334
346
|
}
|
|
335
347
|
}
|
|
336
348
|
|
|
337
|
-
/** Bind the effects table to `unit`, or `fallback` for a texture given none. */
|
|
338
349
|
/** Whether this texture carries an effects table, which the lit program reads only once one exists. */
|
|
339
350
|
get hasEffects(): boolean {
|
|
340
351
|
return this.effectsTable !== null;
|
|
@@ -359,6 +370,7 @@ export class SurfaceTexture {
|
|
|
359
370
|
update(gl: WebGL2RenderingContext, source: TexImageSource): void {
|
|
360
371
|
if (this.texture === null) return;
|
|
361
372
|
refuseArrayUpdate(this.layers);
|
|
373
|
+
refuseBlockUpdate(this.blockFormat);
|
|
362
374
|
const { width, height } = sourceSize(source);
|
|
363
375
|
gl.bindTexture(gl.TEXTURE_2D_ARRAY, this.texture);
|
|
364
376
|
/* Unflipped, matching the constructor. See it for why, and for what it cost. */
|
|
@@ -393,6 +405,65 @@ export class SurfaceTexture {
|
|
|
393
405
|
}
|
|
394
406
|
}
|
|
395
407
|
|
|
408
|
+
/** Images into the bound array, level 0 of each layer; the chain, if any, is generated after. */
|
|
409
|
+
function uploadImages(
|
|
410
|
+
gl: WebGL2RenderingContext,
|
|
411
|
+
sources: readonly TexImageSource[],
|
|
412
|
+
srgb: boolean,
|
|
413
|
+
): void {
|
|
414
|
+
const { width, height } = layerSize(sources);
|
|
415
|
+
/*
|
|
416
|
+
* **Not flipped, and this used to be, which is the bug it is written down for.**
|
|
417
|
+
*
|
|
418
|
+
* The flip was here with a comment saying every source is a 2D drawing surface whose Y
|
|
419
|
+
* runs down from the top-left, so without it "every texture arrives mirrored". That is
|
|
420
|
+
* true of a canvas and **WebGL ignores `UNPACK_FLIP_Y_WEBGL` entirely for an
|
|
421
|
+
* `ImageBitmap`**, which carries its own orientation. So the two source types the same
|
|
422
|
+
* method accepts arrived the opposite way up from each other, and nothing caught it
|
|
423
|
+
* because every canvas-sourced texture in this repository was symmetric under a vertical
|
|
424
|
+
* flip: eroded noise, a radial halo, a sleeve blurred to 24 pixels and back. One consumer
|
|
425
|
+
* painted lettering to a canvas beside a sleeve decoded as a bitmap and the text was
|
|
426
|
+
* upside down.
|
|
427
|
+
*
|
|
428
|
+
* **Made to agree by dropping the flip rather than by extending it**, and the direction
|
|
429
|
+
* is a deliberate choice about blast radius rather than a coin toss: `drftLoader` builds
|
|
430
|
+
* every model texture from an `ImageBitmap`, so flipping bitmaps to match canvases would
|
|
431
|
+
* turn over every painted surface on every loaded model. Dropping it changes only
|
|
432
|
+
* canvas-sourced textures, and every one of those that exists today is symmetric.
|
|
433
|
+
*
|
|
434
|
+
* A caller that was mirroring its own canvas to cancel this must stop.
|
|
435
|
+
*/
|
|
436
|
+
/* Allocated mutable rather than with texStorage3D, so `update` can hand a one-layer texture a
|
|
437
|
+
new size, as texImage2D always could. Each layer is then written in place. */
|
|
438
|
+
gl.texImage3D(
|
|
439
|
+
gl.TEXTURE_2D_ARRAY,
|
|
440
|
+
0,
|
|
441
|
+
srgb ? gl.SRGB8_ALPHA8 : gl.RGBA,
|
|
442
|
+
width,
|
|
443
|
+
height,
|
|
444
|
+
sources.length,
|
|
445
|
+
0,
|
|
446
|
+
gl.RGBA,
|
|
447
|
+
gl.UNSIGNED_BYTE,
|
|
448
|
+
null,
|
|
449
|
+
);
|
|
450
|
+
for (let layer = 0; layer < sources.length; layer++) {
|
|
451
|
+
gl.texSubImage3D(
|
|
452
|
+
gl.TEXTURE_2D_ARRAY,
|
|
453
|
+
0,
|
|
454
|
+
0,
|
|
455
|
+
0,
|
|
456
|
+
layer,
|
|
457
|
+
width,
|
|
458
|
+
height,
|
|
459
|
+
1,
|
|
460
|
+
gl.RGBA,
|
|
461
|
+
gl.UNSIGNED_BYTE,
|
|
462
|
+
sources[layer] as TexImageSource,
|
|
463
|
+
);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
396
467
|
/**
|
|
397
468
|
* Set anisotropic filtering when the driver offers it, and stay silent when it does not.
|
|
398
469
|
*
|