@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
|
@@ -27,6 +27,27 @@
|
|
|
27
27
|
* gates on the same `gradeHere` the mesh pass uses.
|
|
28
28
|
*/
|
|
29
29
|
|
|
30
|
+
/**
|
|
31
|
+
* A highlight shoulder that keeps a colour's hue: past 0.8 the brightest channel eases toward 1 —
|
|
32
|
+
* `0.8 + 0.2·e / (e + 0.2)` of an excess `e` — and the other two are scaled by the same factor, so
|
|
33
|
+
* an overbright red stays red rather than clipping channel by channel toward white.
|
|
34
|
+
*
|
|
35
|
+
* **Per channel is what sRGB alone does, and it is a hue shift**: `(1.6, 0.4, 0.2)` clips to
|
|
36
|
+
* `(1, 0.4, 0.2)`, a pinker colour and a wider core, where this gives `(0.96, 0.24, 0.12)`. The
|
|
37
|
+
* curve is the one a ported game's own resolve applies, read out of its build, so a port matching
|
|
38
|
+
* it reads the same. **What it gives up** is ACES's filmic toe and saturation roll-off: it touches
|
|
39
|
+
* nothing below 0.8 and never desaturates, so a scene that wants a film's look still wants ACES.
|
|
40
|
+
* Shared by the forward passes and the resolve, so the two cannot disagree about it.
|
|
41
|
+
*/
|
|
42
|
+
export const HIGHLIGHT_SHOULDER_GLSL = `
|
|
43
|
+
vec3 highlightShoulder(vec3 c) {
|
|
44
|
+
float m = max(c.r, max(c.g, c.b));
|
|
45
|
+
if (m <= 0.8) return c;
|
|
46
|
+
float e = m - 0.8;
|
|
47
|
+
return c * ((0.8 + 0.2 * e / (e + 0.2)) / m);
|
|
48
|
+
}
|
|
49
|
+
`;
|
|
50
|
+
|
|
30
51
|
/**
|
|
31
52
|
* `uOutputTransform` and `uOutputExposure`, the fit, and `applyOutputTransform`.
|
|
32
53
|
*
|
|
@@ -36,7 +57,7 @@
|
|
|
36
57
|
* a branch that costs one compare.
|
|
37
58
|
*/
|
|
38
59
|
export const OUTPUT_TRANSFORM_GLSL = `
|
|
39
|
-
/** 0 none, 1 sRGB, 2 ACES then sRGB. See renderQuality.ts. */
|
|
60
|
+
/** 0 none, 1 sRGB, 2 ACES then sRGB, 3 the highlight shoulder then sRGB. See renderQuality.ts. */
|
|
40
61
|
uniform int uOutputTransform;
|
|
41
62
|
/** Scales the scene into the tone curve. 1 is the reference grade. See renderQuality.ts. */
|
|
42
63
|
uniform float uOutputExposure;
|
|
@@ -99,9 +120,11 @@ vec3 linearToSrgb(vec3 c) {
|
|
|
99
120
|
return mix(high, low, step(c, vec3(0.0031308)));
|
|
100
121
|
}
|
|
101
122
|
|
|
123
|
+
${HIGHLIGHT_SHOULDER_GLSL}
|
|
102
124
|
vec3 applyOutputTransform(vec3 c) {
|
|
103
125
|
if (uOutputTransform == 0) return c;
|
|
104
126
|
if (uOutputTransform == 2) c = acesFilmic(c);
|
|
127
|
+
if (uOutputTransform == 3) c = highlightShoulder(c * uOutputExposure);
|
|
105
128
|
return linearToSrgb(c);
|
|
106
129
|
}
|
|
107
130
|
`;
|
|
@@ -128,6 +128,108 @@ fn motionSkinnedVert(
|
|
|
128
128
|
return out;
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
+
/*
|
|
132
|
+
* The same for a rig with eight influences a vertex, the second four at the scene's own locations
|
|
133
|
+
* 14 and 15 and summed after the first, as \`skinMatrix()\` sums them with \`SKIN_EIGHT\` on — or the
|
|
134
|
+
* motion of a face or a shoulder would be the four heaviest joints' alone.
|
|
135
|
+
*/
|
|
136
|
+
@vertex
|
|
137
|
+
fn motionSkinnedEightVert(
|
|
138
|
+
@location(0) position: vec3<f32>,
|
|
139
|
+
@location(11) joints: vec4<f32>,
|
|
140
|
+
@location(12) weights: vec4<f32>,
|
|
141
|
+
@location(14) joints2: vec4<f32>,
|
|
142
|
+
@location(15) weights2: vec4<f32>,
|
|
143
|
+
) -> Varyings {
|
|
144
|
+
let skin = skinOf(palette, joints, weights) + skinOf(palette, joints2, weights2);
|
|
145
|
+
let wasSkin =
|
|
146
|
+
skinOf(previousPalette, joints, weights) + skinOf(previousPalette, joints2, weights2);
|
|
147
|
+
let world = draw.model * (skin * vec4<f32>(position, 1.0));
|
|
148
|
+
let wasWorld = draw.previousModel * (wasSkin * vec4<f32>(position, 1.0));
|
|
149
|
+
var out: Varyings;
|
|
150
|
+
out.clip = frame.raster * world;
|
|
151
|
+
out.now = frame.viewProj * world;
|
|
152
|
+
out.was = frame.previousViewProj * wasWorld;
|
|
153
|
+
return out;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/*
|
|
157
|
+
* The cloth-bound stage: a skinned vertex moved toward its simulation triangle by its painted
|
|
158
|
+
* weight, as shaders/clothBinding.ts moves it in the scene, once with this frame's particles and
|
|
159
|
+
* once with last frame's. **The same arithmetic in the same order**, or the depth this stage writes
|
|
160
|
+
* is not the scene's and the equal test admits none of the garment. The binding's turn is not
|
|
161
|
+
* needed: a motion is where a point went, not which way it faces.
|
|
162
|
+
*/
|
|
163
|
+
@group(2) @binding(0) var clothBinding: texture_2d<f32>;
|
|
164
|
+
@group(2) @binding(1) var clothParticles: texture_2d<f32>;
|
|
165
|
+
@group(2) @binding(2) var previousClothParticles: texture_2d<f32>;
|
|
166
|
+
|
|
167
|
+
fn clothTexel(i: i32, width: i32) -> vec2<i32> {
|
|
168
|
+
return vec2<i32>(i % width, i / width);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
fn clothPoint(particles: texture_2d<f32>, indices: vec4<f32>, at: vec4<f32>) -> vec3<f32> {
|
|
172
|
+
let width = i32(textureDimensions(particles, 0).x);
|
|
173
|
+
let a = i32(indices.x);
|
|
174
|
+
let b = i32(indices.y);
|
|
175
|
+
let c = i32(indices.z);
|
|
176
|
+
let pa = textureLoad(particles, clothTexel(a, width), 0).xyz;
|
|
177
|
+
if (a == b && b == c) {
|
|
178
|
+
return pa;
|
|
179
|
+
}
|
|
180
|
+
let pb = textureLoad(particles, clothTexel(b, width), 0).xyz;
|
|
181
|
+
let pc = textureLoad(particles, clothTexel(c, width), 0).xyz;
|
|
182
|
+
let n = normalize(cross(pb - pa, pc - pa));
|
|
183
|
+
return pa * (1.0 - at.x - at.y) + pb * at.x + pc * at.y + n * at.z;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/* A skinned point now and last frame, carried to the cloth by the vertex's own weight. */
|
|
187
|
+
fn clothVaryings(vertex: u32, world: vec4<f32>, wasWorld: vec4<f32>) -> Varyings {
|
|
188
|
+
let width = i32(textureDimensions(clothBinding, 0).x);
|
|
189
|
+
let indices = textureLoad(clothBinding, clothTexel(i32(vertex) * 2, width), 0);
|
|
190
|
+
let at = textureLoad(clothBinding, clothTexel(i32(vertex) * 2 + 1, width), 0);
|
|
191
|
+
let follow = indices.w;
|
|
192
|
+
let placed = vec4<f32>(mix(world.xyz, clothPoint(clothParticles, indices, at), follow), world.w);
|
|
193
|
+
let wasPlaced = vec4<f32>(
|
|
194
|
+
mix(wasWorld.xyz, clothPoint(previousClothParticles, indices, at), follow),
|
|
195
|
+
wasWorld.w);
|
|
196
|
+
var out: Varyings;
|
|
197
|
+
out.clip = frame.raster * placed;
|
|
198
|
+
out.now = frame.viewProj * placed;
|
|
199
|
+
out.was = frame.previousViewProj * wasPlaced;
|
|
200
|
+
return out;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
@vertex
|
|
204
|
+
fn motionClothVert(
|
|
205
|
+
@builtin(vertex_index) vertex: u32,
|
|
206
|
+
@location(0) position: vec3<f32>,
|
|
207
|
+
@location(11) joints: vec4<f32>,
|
|
208
|
+
@location(12) weights: vec4<f32>,
|
|
209
|
+
) -> Varyings {
|
|
210
|
+
let world = draw.model * (skinOf(palette, joints, weights) * vec4<f32>(position, 1.0));
|
|
211
|
+
let wasWorld =
|
|
212
|
+
draw.previousModel * (skinOf(previousPalette, joints, weights) * vec4<f32>(position, 1.0));
|
|
213
|
+
return clothVaryings(vertex, world, wasWorld);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
@vertex
|
|
217
|
+
fn motionClothEightVert(
|
|
218
|
+
@builtin(vertex_index) vertex: u32,
|
|
219
|
+
@location(0) position: vec3<f32>,
|
|
220
|
+
@location(11) joints: vec4<f32>,
|
|
221
|
+
@location(12) weights: vec4<f32>,
|
|
222
|
+
@location(14) joints2: vec4<f32>,
|
|
223
|
+
@location(15) weights2: vec4<f32>,
|
|
224
|
+
) -> Varyings {
|
|
225
|
+
let skin = skinOf(palette, joints, weights) + skinOf(palette, joints2, weights2);
|
|
226
|
+
let wasSkin =
|
|
227
|
+
skinOf(previousPalette, joints, weights) + skinOf(previousPalette, joints2, weights2);
|
|
228
|
+
let world = draw.model * (skin * vec4<f32>(position, 1.0));
|
|
229
|
+
let wasWorld = draw.previousModel * (wasSkin * vec4<f32>(position, 1.0));
|
|
230
|
+
return clothVaryings(vertex, world, wasWorld);
|
|
231
|
+
}
|
|
232
|
+
|
|
131
233
|
/*
|
|
132
234
|
* The rewritten stage: a mesh whose vertices were rewritten this frame reads last frame's rows from
|
|
133
235
|
* a second buffer of the same stride, so each vertex carries both of its positions.
|
|
@@ -951,6 +951,19 @@ fn reconSceneBilinear(p: vec2<f32>) -> vec3<f32> {
|
|
|
951
951
|
return textureSampleLevel(sceneTexture, linearSampler, uv, 0.0).xyz;
|
|
952
952
|
}
|
|
953
953
|
|
|
954
|
+
/*
|
|
955
|
+
* The surface's share of an output pixel, read from the jittered scene it was rebuilt from
|
|
956
|
+
* (sceneCoverage.ts): what the blended draws that landed before the reconstruction left of it.
|
|
957
|
+
* The late ones then cover the picture this is stored in, so the composite's occlusion reads both.
|
|
958
|
+
* Bilinear and at the unjittered position, which is where the colour beside it stands.
|
|
959
|
+
*/
|
|
960
|
+
fn reconSceneShare(ox: i32, oy: i32) -> f32 {
|
|
961
|
+
let output = vec2<f32>(f32(recon.outputSize.x), f32(recon.outputSize.y));
|
|
962
|
+
let render = vec2<f32>(f32(recon.renderSize.x), f32(recon.renderSize.y));
|
|
963
|
+
let at = (vec2<f32>(f32(ox), f32(oy)) + 0.5) / output + recon.jitter / render;
|
|
964
|
+
return textureSampleLevel(sceneTexture, linearSampler, vec2<f32>(at.x, 1.0 - at.y), 0.0).w;
|
|
965
|
+
}
|
|
966
|
+
|
|
954
967
|
fn reconHistoryBilinear(p: vec2<f32>) -> vec4<f32> {
|
|
955
968
|
let size = vec2<f32>(f32(recon.outputSize.x), f32(recon.outputSize.y));
|
|
956
969
|
let uv = vec2<f32>(p.x + 0.5, size.y - 0.5 - p.y) / size;
|
|
@@ -973,7 +986,8 @@ fn sharpenMain(@builtin(global_invocation_id) id: vec3<u32>) {
|
|
|
973
986
|
if (id.x >= recon.outputSize.x || id.y >= recon.outputSize.y) { return; }
|
|
974
987
|
let y = i32(recon.outputSize.y) - 1 - i32(id.y);
|
|
975
988
|
let value = reconSharpenAt(i32(id.x), y);
|
|
976
|
-
|
|
989
|
+
let share = reconSceneShare(i32(id.x), y);
|
|
990
|
+
textureStore(resolved, vec2<i32>(i32(id.x), i32(id.y)), vec4<f32>(value, share));
|
|
977
991
|
}
|
|
978
992
|
`;
|
|
979
993
|
}
|
|
@@ -25,6 +25,7 @@ import { DEPTH_OF_FIELD_GLSL } from './depthOfField.ts';
|
|
|
25
25
|
import { AUTO_EXPOSURE_GLSL } from './exposure.ts';
|
|
26
26
|
import { LOCAL_EXPOSURE_GLSL } from './localExposure.ts';
|
|
27
27
|
import { FILM_LOOK_GLSL } from './filmLook.ts';
|
|
28
|
+
import { HIGHLIGHT_SHOULDER_GLSL } from './outputTransform.ts';
|
|
28
29
|
|
|
29
30
|
export const RUSH_FRAG = `#version 300 es
|
|
30
31
|
precision highp float;
|
|
@@ -242,9 +243,11 @@ vec3 acesFilmic(vec3 x) {
|
|
|
242
243
|
return clamp(ACES_OUTPUT * (a / b), 0.0, 1.0);
|
|
243
244
|
}
|
|
244
245
|
|
|
246
|
+
${HIGHLIGHT_SHOULDER_GLSL}
|
|
245
247
|
vec3 grade(vec3 c) {
|
|
246
248
|
if (uOutputTransform == 0) return c;
|
|
247
249
|
if (uOutputTransform == 2) c = acesFilmic(c);
|
|
250
|
+
if (uOutputTransform == 3) c = highlightShoulder(c * uOutputExposure);
|
|
248
251
|
return linearToSrgb(c);
|
|
249
252
|
}
|
|
250
253
|
|
|
@@ -335,7 +338,8 @@ vec3 finish(vec3 light) {
|
|
|
335
338
|
}
|
|
336
339
|
|
|
337
340
|
void main() {
|
|
338
|
-
|
|
341
|
+
vec4 sampled = textureLod(uScene, vUv, 0.0);
|
|
342
|
+
vec3 scene = sampled.rgb;
|
|
339
343
|
if (uMotionStrength > 0.0) scene = cameraBlur(scene);
|
|
340
344
|
/*
|
|
341
345
|
* Defocus after the camera smear and before everything else, because a lens is the last thing
|
|
@@ -350,8 +354,15 @@ void main() {
|
|
|
350
354
|
* not then darkened again along its own trail. mix from 1 rather than a multiply by the
|
|
351
355
|
* strength, so the parameter reads as "how much of this occlusion", and 0 is exactly the
|
|
352
356
|
* frame that existed before the effect did.
|
|
357
|
+
*
|
|
358
|
+
* **Scaled by the surface's share of the pixel**, which the scene carries in its alpha
|
|
359
|
+
* (sceneCoverage.ts): the occlusion was measured from the depth the opaque world left, and a
|
|
360
|
+
* pane, a puff of smoke or a caption in front of a corner wrote no depth, so darkening all of the
|
|
361
|
+
* pixel darkened them by a corner they hide. Where nothing blended landed the share is 1 and
|
|
362
|
+
* this is the frame it was.
|
|
353
363
|
*/
|
|
354
|
-
float
|
|
364
|
+
float share = clamp(sampled.a, 0.0, 1.0);
|
|
365
|
+
float ao = mix(1.0, textureLod(uAo, vUv + uAoOffset, 0.0).r, uAoStrength * share);
|
|
355
366
|
|
|
356
367
|
/*
|
|
357
368
|
* Distance from the centre, corrected so the falloff is a circle on screen rather than
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skin's screen-space scattering: one axis of the blur that spreads the diffuse pass by Burley's
|
|
3
|
+
* profile, and on the second axis the sum the frame takes back. `skinBlur.ts` beside the render
|
|
4
|
+
* directory is the kernel's reference and says what it gives up.
|
|
5
|
+
*
|
|
6
|
+
* **One pass, run twice**: along x into a target of its own, keeping the profile in the alpha, then
|
|
7
|
+
* along y blended additively into the frame — its colour added, the frame's alpha kept.
|
|
8
|
+
*
|
|
9
|
+
* **Refuses what is not the same skin**: a neighbour that wrote no skin, or one further in depth
|
|
10
|
+
* than four of the profile's widest distances, takes no weight, and what is kept is divided by
|
|
11
|
+
* what was kept, so a cheek's light does not pull toward the black beside its silhouette. A tap
|
|
12
|
+
* part skin is counted for the part it is: the target is read as coverage-weighted, which is what
|
|
13
|
+
* a filtered tap across an edge already is.
|
|
14
|
+
*/
|
|
15
|
+
import { glslIsFarDepth, glslSceneDepthToNdc } from '../depthConvention.ts';
|
|
16
|
+
import { SKIN_BLUR_EDGES, SKIN_BLUR_TAPS, SKIN_PROFILES } from '../skinBlur.ts';
|
|
17
|
+
|
|
18
|
+
const list = (values: readonly number[]): string => values.map((v) => v.toFixed(2)).join(', ');
|
|
19
|
+
|
|
20
|
+
export const SKIN_BLUR_FRAG = `#version 300 es
|
|
21
|
+
precision highp float;
|
|
22
|
+
|
|
23
|
+
in vec2 vUv;
|
|
24
|
+
|
|
25
|
+
/** The diffuse pass, or the first axis' result: diffuse in rgb, the profile as (index + 1) / 8. */
|
|
26
|
+
uniform sampler2D uSkin;
|
|
27
|
+
/** highp, for the reason the ambient occlusion blur gives: eight bits of depth find false edges. */
|
|
28
|
+
uniform highp sampler2D uDepth;
|
|
29
|
+
/** One texel along the axis being blurred, in UV. The other component is zero. */
|
|
30
|
+
uniform vec2 uStep;
|
|
31
|
+
/** A depth sample to view-space metres, as the ambient occlusion blur takes it. */
|
|
32
|
+
uniform vec4 uDepthToViewZ;
|
|
33
|
+
/** How many pixels a metre spans at a metre's distance. */
|
|
34
|
+
uniform float uFocal;
|
|
35
|
+
/** Each profile's scatter distance per channel, in metres. */
|
|
36
|
+
uniform vec4 uProfiles[${SKIN_PROFILES}];
|
|
37
|
+
|
|
38
|
+
out vec4 fragColor;
|
|
39
|
+
|
|
40
|
+
const float TAPS[${SKIN_BLUR_TAPS.length}] = float[${SKIN_BLUR_TAPS.length}](${list(SKIN_BLUR_TAPS)});
|
|
41
|
+
const float EDGES[${SKIN_BLUR_EDGES.length}] = float[${SKIN_BLUR_EDGES.length}](${list(SKIN_BLUR_EDGES)});
|
|
42
|
+
|
|
43
|
+
float viewZ(float depth) {
|
|
44
|
+
float ndc = ${glslSceneDepthToNdc('depth')};
|
|
45
|
+
return (uDepthToViewZ.x * ndc + uDepthToViewZ.y) / (uDepthToViewZ.z * ndc + uDepthToViewZ.w);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Burley's profile across a line, integrated from 0 to x: one half at infinity. */
|
|
49
|
+
vec3 burleyMass(float x, vec3 d) {
|
|
50
|
+
return 0.5 - (exp(-x / d) + 3.0 * exp(-x / (3.0 * d))) * 0.125;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
void main() {
|
|
54
|
+
vec4 own = textureLod(uSkin, vUv, 0.0);
|
|
55
|
+
/*
|
|
56
|
+
* Not skin: nothing here to spread, and nothing to add. Any alpha is skin, because a pixel a
|
|
57
|
+
* silhouette crosses under multisampling resolves to its covered share of the profile's code, and
|
|
58
|
+
* its colour to that share of the diffuse, which is exactly what it owes the frame.
|
|
59
|
+
*/
|
|
60
|
+
if (own.a < 1e-4) {
|
|
61
|
+
fragColor = vec4(0.0);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
float depth = textureLod(uDepth, vUv, 0.0).r;
|
|
65
|
+
if (${glslIsFarDepth('depth')}) {
|
|
66
|
+
fragColor = own;
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
/* A part-covered pixel's code is its share of the whole: read it as the nearest, which is the
|
|
70
|
+
right profile at half coverage and up, and a neighbouring one only on a silhouette below it. */
|
|
71
|
+
int profile = clamp(int(own.a * 8.0 + 0.5) - 1, 0, ${SKIN_PROFILES - 1});
|
|
72
|
+
vec3 d = max(uProfiles[profile].xyz, vec3(1e-6));
|
|
73
|
+
float widest = max(d.r, max(d.g, d.b));
|
|
74
|
+
float z = abs(viewZ(depth));
|
|
75
|
+
/* The widest distance in pixels here: under half of one, there is nothing to spread across. */
|
|
76
|
+
float pixels = widest * uFocal / max(z, 1e-4);
|
|
77
|
+
if (pixels < 0.5) {
|
|
78
|
+
fragColor = own;
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
vec3 centre = 2.0 * burleyMass(EDGES[0] * widest, d);
|
|
82
|
+
vec3 sum = own.rgb * centre;
|
|
83
|
+
vec3 weight = centre;
|
|
84
|
+
for (int i = 1; i < ${SKIN_BLUR_TAPS.length}; i++) {
|
|
85
|
+
vec3 outer = i < ${SKIN_BLUR_EDGES.length} ? burleyMass(EDGES[min(i, ${SKIN_BLUR_EDGES.length - 1})] * widest, d) : vec3(0.5);
|
|
86
|
+
vec3 w = outer - burleyMass(EDGES[i - 1] * widest, d);
|
|
87
|
+
for (int side = -1; side <= 1; side += 2) {
|
|
88
|
+
vec2 uv = vUv + uStep * (float(side) * TAPS[i] * pixels);
|
|
89
|
+
vec4 tap = textureLod(uSkin, uv, 0.0);
|
|
90
|
+
float tapZ = abs(viewZ(textureLod(uDepth, uv, 0.0).r));
|
|
91
|
+
float keep = clamp(1.0 - abs(tapZ - z) / (4.0 * widest), 0.0, 1.0);
|
|
92
|
+
/*
|
|
93
|
+
* How much of the tap is skin: a filtered tap astride a silhouette is part skin and part
|
|
94
|
+
* the black beside it, in colour and in alpha alike, so its alpha over this pixel's says the
|
|
95
|
+
* share, and the colour is already that share of the skin's — counted as such, it neither
|
|
96
|
+
* darkens the edge nor drops out.
|
|
97
|
+
*/
|
|
98
|
+
float cover = clamp(tap.a / own.a, 0.0, 1.0);
|
|
99
|
+
sum += tap.rgb * w * keep;
|
|
100
|
+
weight += w * (cover * keep);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
fragColor = vec4(sum / weight, own.a);
|
|
104
|
+
}
|
|
105
|
+
`;
|
|
@@ -31,6 +31,18 @@ uniform highp sampler2D uJointPalette;
|
|
|
31
31
|
|
|
32
32
|
layout(location = 11) in vec4 aJoints;
|
|
33
33
|
layout(location = 12) in vec4 aWeights;
|
|
34
|
+
/*
|
|
35
|
+
* **The fifth to eighth influences, read only by a pipeline built for a mesh that has them.**
|
|
36
|
+
*
|
|
37
|
+
* Declared in every skinned variant at the last two locations WebGL2 guarantees — 13 is the channel,
|
|
38
|
+
* and an instanced draw, which takes 11 to 15, cannot skin — and fed zeros by the absent-attribute
|
|
39
|
+
* constants for a mesh with four. \`SKIN_EIGHT\` is a pipeline constant rather than a permutation,
|
|
40
|
+
* so the generated vertex stages do not double: off, the device compiles the second sum away and a
|
|
41
|
+
* four-influence mesh pays nothing; on, it is four more palette reads a vertex.
|
|
42
|
+
*/
|
|
43
|
+
const bool SKIN_EIGHT = false; // wgsl:override
|
|
44
|
+
layout(location = 14) in vec4 aJoints2;
|
|
45
|
+
layout(location = 15) in vec4 aWeights2;
|
|
34
46
|
|
|
35
47
|
/** One joint's matrix, read as four consecutive texels of row 0. */
|
|
36
48
|
mat4 jointMatrix(int index) {
|
|
@@ -51,9 +63,24 @@ mat4 jointMatrix(int index) {
|
|
|
51
63
|
* by hand with unnormalised weights, which shades slightly dark or bright rather than breaking.
|
|
52
64
|
*/
|
|
53
65
|
mat4 skinMatrix() {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
66
|
+
mat4 m = jointMatrix(int(aJoints.x)) * aWeights.x
|
|
67
|
+
+ jointMatrix(int(aJoints.y)) * aWeights.y
|
|
68
|
+
+ jointMatrix(int(aJoints.z)) * aWeights.z
|
|
69
|
+
+ jointMatrix(int(aJoints.w)) * aWeights.w;
|
|
70
|
+
if (SKIN_EIGHT) {
|
|
71
|
+
m += jointMatrix(int(aJoints2.x)) * aWeights2.x
|
|
72
|
+
+ jointMatrix(int(aJoints2.y)) * aWeights2.y
|
|
73
|
+
+ jointMatrix(int(aJoints2.z)) * aWeights2.z
|
|
74
|
+
+ jointMatrix(int(aJoints2.w)) * aWeights2.w;
|
|
75
|
+
}
|
|
76
|
+
return m;
|
|
58
77
|
}
|
|
78
|
+
|
|
59
79
|
`;
|
|
80
|
+
|
|
81
|
+
/** The source above with the eight-influence switch on, for a WebGL2 program built for such a mesh. */
|
|
82
|
+
export function skinEight(source: string): string {
|
|
83
|
+
const off = 'const bool SKIN_EIGHT = false;';
|
|
84
|
+
if (!source.includes(off)) throw new Error('skinEight: the source carries no SKIN_EIGHT switch');
|
|
85
|
+
return source.replace(off, 'const bool SKIN_EIGHT = true;');
|
|
86
|
+
}
|
|
@@ -211,11 +211,18 @@ vec3 historyCatmullRom(vec2 uv) {
|
|
|
211
211
|
}
|
|
212
212
|
|
|
213
213
|
void main() {
|
|
214
|
-
|
|
214
|
+
/*
|
|
215
|
+
* **The alpha is this frame's and takes no history.** It is how much of the pixel is still the
|
|
216
|
+
* opaque surface (sceneCoverage.ts), which the composite's occlusion reads after this has
|
|
217
|
+
* replaced the scene, so it passes through untouched. The history is read for its colour alone.
|
|
218
|
+
*/
|
|
219
|
+
vec4 sampled = textureLod(uScene, vUv, 0.0);
|
|
220
|
+
vec3 current = sampled.rgb;
|
|
221
|
+
float share = sampled.a;
|
|
215
222
|
|
|
216
223
|
/* Nothing to blend towards: the first frame, a resize, a cut, or the effect switched off. */
|
|
217
224
|
if (uHistoryBlend <= 0.0) {
|
|
218
|
-
fragColor = vec4(current,
|
|
225
|
+
fragColor = vec4(current, share);
|
|
219
226
|
flickerOut = freshRecord(current);
|
|
220
227
|
flickerMotionOut = vec4(0.0);
|
|
221
228
|
return;
|
|
@@ -248,7 +255,7 @@ void main() {
|
|
|
248
255
|
vec4 previous = uReprojection * clip;
|
|
249
256
|
/* Behind the previous eye: there is no last-frame position to sample. */
|
|
250
257
|
if (previous.w <= 0.0) {
|
|
251
|
-
fragColor = vec4(current,
|
|
258
|
+
fragColor = vec4(current, share);
|
|
252
259
|
flickerOut = freshRecord(current);
|
|
253
260
|
flickerMotionOut = vec4(metres, 0.0, 0.0, 0.0);
|
|
254
261
|
return;
|
|
@@ -261,7 +268,7 @@ void main() {
|
|
|
261
268
|
* as a smear for as long as the camera kept turning.
|
|
262
269
|
*/
|
|
263
270
|
if (wasUv.x < 0.0 || wasUv.x > 1.0 || wasUv.y < 0.0 || wasUv.y > 1.0) {
|
|
264
|
-
fragColor = vec4(current,
|
|
271
|
+
fragColor = vec4(current, share);
|
|
265
272
|
flickerOut = freshRecord(current);
|
|
266
273
|
flickerMotionOut = vec4(metres, 0.0, 0.0, 0.0);
|
|
267
274
|
return;
|
|
@@ -320,6 +327,6 @@ void main() {
|
|
|
320
327
|
repeated * still
|
|
321
328
|
);
|
|
322
329
|
flickerMotionOut = vec4(metres, swingOut, 0.0, 0.0);
|
|
323
|
-
fragColor = vec4(mix(current, bounded, blend),
|
|
330
|
+
fragColor = vec4(mix(current, bounded, blend), share);
|
|
324
331
|
}
|
|
325
332
|
`;
|
|
@@ -2,6 +2,7 @@ import { resolveGlass, type GlassOptions, type ResolvedGlass } from './glass.ts'
|
|
|
2
2
|
import type { ReadonlyMat4 } from 'gl-matrix';
|
|
3
3
|
import type { InstanceData } from './instancedMesh.ts';
|
|
4
4
|
import type { InstancedHandle, MeshHandle, ScatterHandle } from './backend/api.ts';
|
|
5
|
+
import type { ClothBindingHandle, ClothParticlesHandle } from './backend/api.ts';
|
|
5
6
|
import type { MeshInstances } from './instances.ts';
|
|
6
7
|
import type { SurfaceMaterial } from './surfaceTexture.ts';
|
|
7
8
|
import type { SurfaceTextureHandle } from './backend/api.ts';
|
|
@@ -70,6 +71,12 @@ export function glassOf(material: SceneCasterMaterial | undefined, out: Resolved
|
|
|
70
71
|
return resolveGlass(material?.glass, out);
|
|
71
72
|
}
|
|
72
73
|
|
|
74
|
+
/** A skinned caster's cloth: the mesh's binding and the character's particles. See `setCloth`. */
|
|
75
|
+
export interface ShadowCasterCloth {
|
|
76
|
+
readonly binding: ClothBindingHandle;
|
|
77
|
+
readonly particles: ClothParticlesHandle;
|
|
78
|
+
}
|
|
79
|
+
|
|
73
80
|
export interface ShadowCasterSink {
|
|
74
81
|
/** A rigid mesh at a model transform: the world, a prop, a static piece of scenery. */
|
|
75
82
|
mesh(mesh: MeshHandle, model: ReadonlyMat4, material?: SceneCasterMaterial): void;
|
|
@@ -88,6 +95,12 @@ export interface ShadowCasterSink {
|
|
|
88
95
|
model: ReadonlyMat4,
|
|
89
96
|
palette: Float32Array,
|
|
90
97
|
material?: SceneCasterMaterial,
|
|
98
|
+
/**
|
|
99
|
+
* The cloth the visible draw places this mesh by, if any — for the reason the palette is not
|
|
100
|
+
* optional in spirit: a garment cast from its skinning alone throws a shadow that does not
|
|
101
|
+
* move as the cloth does.
|
|
102
|
+
*/
|
|
103
|
+
cloth?: ShadowCasterCloth,
|
|
91
104
|
): void;
|
|
92
105
|
/**
|
|
93
106
|
* An instanced batch of rigid meshes, placed by the same matrices the visible draw uses.
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skin's screen-space scattering, as `shaders/skinBlur.ts` spreads it: the reference its tests hold
|
|
3
|
+
* the kernel to, and nothing at run time calls it.
|
|
4
|
+
*
|
|
5
|
+
* **What it spreads.** With `skinScattering: 'screen-space'`, a skin draw writes its specular to the
|
|
6
|
+
* frame and its diffuse — the light that went beneath the surface — to a target of its own, with the
|
|
7
|
+
* profile it scatters by in the alpha. Before anything blended is drawn, that target is blurred
|
|
8
|
+
* along the screen's two axes by Burley's normalised diffusion, one width per channel, and added
|
|
9
|
+
* back: light that entered a cheek leaves it a centimetre away, reddest furthest, across the
|
|
10
|
+
* pixels that centimetre covers at the cheek's distance. The pre-integrated fit stays in the lit
|
|
11
|
+
* stage for what this cannot see — a terminator's curvature — and this carries what that cannot:
|
|
12
|
+
* light crossing a shadow's edge, or a nostril's rim, in the picture rather than in a fit.
|
|
13
|
+
*
|
|
14
|
+
* **Separable, which the profile is not**: two 1D passes with Burley's profile integrated across a
|
|
15
|
+
* line, as Jimenez's separable subsurface scattering does. What it gives up is the radial shape, a
|
|
16
|
+
* blur a little squarer than the profile; what it buys is thirteen taps an axis rather than a disc.
|
|
17
|
+
*
|
|
18
|
+
* **Each tap is the profile's mass over its cell, in closed form**, so a channel's weights sum to
|
|
19
|
+
* one before any are refused, and a channel that travels a quarter as far as the widest keeps most
|
|
20
|
+
* of its light at the centre. The taps are placed in units of the widest channel's distance and
|
|
21
|
+
* closer together near the centre, where the profile's sharp peak is.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/** The taps' distances from the centre, one side, in units of the widest channel's distance. */
|
|
25
|
+
export const SKIN_BLUR_TAPS = [0, 0.4, 1.1, 2.1, 3.5, 5.6, 9] as const;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Where each tap's cell ends, in the same units: halfway to the next tap, and the last cell runs
|
|
29
|
+
* to infinity, so the profile's whole mass is somewhere.
|
|
30
|
+
*/
|
|
31
|
+
export const SKIN_BLUR_EDGES = [0.2, 0.75, 1.6, 2.8, 4.55, 7.3] as const;
|
|
32
|
+
|
|
33
|
+
/** How many profiles a frame's skin can name: `SkinModel.profile`. */
|
|
34
|
+
export const SKIN_PROFILES = 8;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Burley's profile across a line, `(e^(−x/d) + e^(−x/3d)) / 8d`, integrated from 0 to `x`: half of
|
|
38
|
+
* it lies on each side, so this reaches ½ at infinity.
|
|
39
|
+
*/
|
|
40
|
+
export function burleyMass(x: number, d: number): number {
|
|
41
|
+
return 0.5 - (Math.exp(-x / d) + 3 * Math.exp(-x / (3 * d))) / 8;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Tap `tap`'s weight for one channel scattering `d`, the taps spaced by `widest`: the centre's cell
|
|
46
|
+
* is both sides of it, every other cell one side, and a tap on each side takes this weight.
|
|
47
|
+
*/
|
|
48
|
+
export function skinBlurWeight(tap: number, d: number, widest: number): number {
|
|
49
|
+
if (tap === 0) return 2 * burleyMass((SKIN_BLUR_EDGES[0] as number) * widest, d);
|
|
50
|
+
const inner = (SKIN_BLUR_EDGES[tap - 1] as number) * widest;
|
|
51
|
+
const outer = tap < SKIN_BLUR_EDGES.length ? (SKIN_BLUR_EDGES[tap] as number) * widest : Infinity;
|
|
52
|
+
return (outer === Infinity ? 0.5 : burleyMass(outer, d)) - burleyMass(inner, d);
|
|
53
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The skin model's arithmetic, as `shaders/flat/skin.ts` computes it: the reference its tests hold
|
|
3
|
+
* the fit to, and nothing at run time calls it.
|
|
4
|
+
*
|
|
5
|
+
* **Pre-integrated, after Penner 2011, with the profile integrated rather than looked up.** Light
|
|
6
|
+
* entering skin leaves it a little way off, so on a curved surface a point is lit by its
|
|
7
|
+
* neighbours' `N·L` as well as its own: the clamped cosine averaged around a ring of the surface's
|
|
8
|
+
* curvature, weighted by the diffusion profile at each neighbour's distance. The profile is Burley's
|
|
9
|
+
* normalised diffusion, one per channel, at a scatter distance `d` — the model's `radius` times its
|
|
10
|
+
* scatter colour — projected onto the ring's plane, as a cylinder's cross-section sees it. The
|
|
11
|
+
* result depends on `N·L` and on one number per channel, `k = d × curvature`, and at `k = 0` it is
|
|
12
|
+
* Lambert's.
|
|
13
|
+
*
|
|
14
|
+
* **Fitted rather than tabulated**, so no seventeenth sampler and no image: the ring average is the
|
|
15
|
+
* clamped cosine convolved with an angular kernel, so it splits into the cosine's first harmonic —
|
|
16
|
+
* scaled by one number, `κ₁(k)` — and a smoothed `|cos θ|` carrying every even harmonic:
|
|
17
|
+
*
|
|
18
|
+
* D(c, k) = ½ (κ₁ c + A √(c² + ε²) + B)
|
|
19
|
+
*
|
|
20
|
+
* with `κ₁`, `A`, `ε` and `B` closed forms in `k` that are exactly `1, 1, 0, 0` at `k = 0`. The
|
|
21
|
+
* test measures it against the integral done numerically: **worst 0.0105 over `k` to 10**, beyond
|
|
22
|
+
* which the shader holds `k` — a scatter ten times the surface's own radius, where the answer is
|
|
23
|
+
* nearly flat already.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
/** Schlick's base for skin, at index 1.4: `((1 − n) / (1 + n))²`. */
|
|
27
|
+
export const SKIN_F0 = ((1 - 1.4) / (1 + 1.4)) ** 2;
|
|
28
|
+
|
|
29
|
+
/** The largest `k` the fit is held to, and where the shader clamps it. */
|
|
30
|
+
export const SKIN_MAX_K = 10;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The fit's eight numbers, found by minimising its worst error against the numeric integral over
|
|
34
|
+
* `c` in [−1, 1] and `k` in (0, 10]:
|
|
35
|
+
*
|
|
36
|
+
* - `kappa`: `κ₁ = (1 + a k² + b k³)^(−1/6)` — the first harmonic's share, quadratic at zero and
|
|
37
|
+
* falling as `1/√k`, as the integral's does;
|
|
38
|
+
* - `spread`: `A = (1 + a k³)^(−1/6)`;
|
|
39
|
+
* - `soften`: `ε = a k / (1 + b k)`, how far `|c|` is rounded at the terminator;
|
|
40
|
+
* - `floor`: `B = (1 − A)(a + b (1 − A))`, the even part's floor as `A` gives up `|c|`.
|
|
41
|
+
*/
|
|
42
|
+
export const SKIN_FIT = {
|
|
43
|
+
kappa: [22.6772, 101.349],
|
|
44
|
+
spread: 838.87,
|
|
45
|
+
soften: [1.3081, 3.65743],
|
|
46
|
+
floor: [0.508537, 0.129496],
|
|
47
|
+
} as const;
|
|
48
|
+
|
|
49
|
+
/** The ring-averaged `N·L` for one channel: `c` is `N·L`, `k` that channel's scatter × curvature. */
|
|
50
|
+
export function skinRing(c: number, k: number): number {
|
|
51
|
+
const q = Math.min(Math.max(k, 0), SKIN_MAX_K);
|
|
52
|
+
const k2 = q * q;
|
|
53
|
+
const kappa = (1 + SKIN_FIT.kappa[0] * k2 + SKIN_FIT.kappa[1] * k2 * q) ** (-1 / 6);
|
|
54
|
+
const spread = (1 + SKIN_FIT.spread * k2 * q) ** (-1 / 6);
|
|
55
|
+
const soften = (SKIN_FIT.soften[0] * q) / (1 + SKIN_FIT.soften[1] * q);
|
|
56
|
+
const given = 1 - spread;
|
|
57
|
+
const floor = given * (SKIN_FIT.floor[0] + SKIN_FIT.floor[1] * given);
|
|
58
|
+
return 0.5 * (kappa * c + spread * Math.sqrt(c * c + soften * soften) + floor);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* How far into a penumbra skin's light reaches, as a width the shadow is assumed to have: the
|
|
63
|
+
* lamp loop reads a shadow where its derivative cannot be taken, so its width in the world is not
|
|
64
|
+
* known there. Two centimetres is a soft edge across a face.
|
|
65
|
+
*/
|
|
66
|
+
export const SKIN_PENUMBRA_WIDTH = 0.02;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* A shadow on skin, one channel at scatter distance `d`: the shadow's own value raised to
|
|
70
|
+
* `1 / (1 + d / width)`, so light reaches further into the penumbra the further that channel
|
|
71
|
+
* travels beneath the surface — red furthest, which is the red edge a shadow on skin has — and a
|
|
72
|
+
* shadow stays whole at 0 and absent at 1. **What it gives up** is the profile itself: Penner
|
|
73
|
+
* convolves a sharpened shadow with it, which needs the penumbra's width in the world and hardens
|
|
74
|
+
* the narrow channels' edge where nothing widened the map to make room.
|
|
75
|
+
*/
|
|
76
|
+
export function skinPenumbra(shadow: number, d: number): number {
|
|
77
|
+
return Math.max(shadow, 0) ** (1 / (1 + d / SKIN_PENUMBRA_WIDTH));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Light through a thin part, one channel: `transmission × exp(−thickness / d)` of the light behind,
|
|
82
|
+
* by how squarely it is behind. A part as thick as its curvature's diameter — an ear's rim at a few
|
|
83
|
+
* millimetres, a cheek at ten centimetres — passes red through the one and nothing through the other.
|
|
84
|
+
*/
|
|
85
|
+
export function skinTransmission(
|
|
86
|
+
backNdl: number,
|
|
87
|
+
thickness: number,
|
|
88
|
+
d: number,
|
|
89
|
+
transmission: number,
|
|
90
|
+
): number {
|
|
91
|
+
return transmission * Math.exp(-thickness / Math.max(d, 1e-6)) * Math.max(backNdl, 0);
|
|
92
|
+
}
|