@driftengine/core 4.8.3 → 4.8.5
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 +82 -0
- package/README.md +1 -1
- package/dist/build/shaderComments.js +0 -0
- package/dist/geometry/meshConcat.js +2 -0
- package/dist/index.d.ts +10 -4
- package/dist/index.js +16 -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/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 +136 -3
- package/dist/render/backend/webgl2/renderer.js +604 -63
- 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/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 +16 -6
- package/dist/render/backend/webgpu/depthPass.js +39 -9
- package/dist/render/backend/webgpu/drawCommand.js +6 -2
- package/dist/render/backend/webgpu/flatPass.d.ts +38 -4
- package/dist/render/backend/webgpu/flatPass.js +109 -12
- package/dist/render/backend/webgpu/gpuSkinnedCloth.d.ts +74 -0
- package/dist/render/backend/webgpu/gpuSkinnedCloth.js +452 -0
- package/dist/render/backend/webgpu/motionPass.d.ts +20 -0
- package/dist/render/backend/webgpu/motionPass.js +106 -9
- package/dist/render/backend/webgpu/postPass.d.ts +3 -1
- package/dist/render/backend/webgpu/postPass.js +6 -3
- package/dist/render/backend/webgpu/renderer.d.ts +113 -5
- package/dist/render/backend/webgpu/renderer.js +498 -48
- 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/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 +2 -0
- package/dist/render/cutoutCaster.js +2 -0
- package/dist/render/cutoutDither.d.ts +51 -0
- package/dist/render/cutoutDither.js +90 -0
- 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/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/mesh.d.ts +7 -0
- package/dist/render/mesh.js +24 -0
- package/dist/render/recon/motionVectors.d.ts +4 -2
- package/dist/render/recon/motionVectors.js +6 -3
- package/dist/render/renderQuality.d.ts +20 -0
- package/dist/render/renderQuality.js +5 -0
- package/dist/render/rendererHarness.js +4 -0
- package/dist/render/sceneTarget.d.ts +12 -0
- package/dist/render/sceneTarget.js +14 -0
- package/dist/render/shaders/clothBinding.d.ts +22 -0
- package/dist/render/shaders/clothBinding.js +80 -0
- package/dist/render/shaders/depth.js +42 -6
- 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 +36 -2
- 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 +38 -16
- package/dist/render/shaders/generated/depth.wgsl.js +38 -16
- package/dist/render/shaders/generated/flat.wgsl.d.ts +1790 -1704
- package/dist/render/shaders/generated/flat.wgsl.js +1828 -1742
- 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/recon/motion.wgsl.d.ts +1 -1
- package/dist/render/shaders/recon/motion.wgsl.js +102 -0
- 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/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/surfaceModel.d.ts +129 -0
- package/dist/render/surfaceModel.js +152 -0
- package/dist/render/surfaceTexture.d.ts +44 -4
- package/dist/render/surfaceTexture.js +60 -46
- package/dist/render/vertexDefaults.js +7 -0
- package/package.json +6 -6
- package/src/build/shaderComments.ts +0 -0
- package/src/geometry/meshConcat.ts +2 -0
- package/src/index.ts +40 -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/select.ts +8 -0
- package/src/render/backend/webgl2/clothTextures.ts +120 -0
- package/src/render/backend/webgl2/renderer.ts +693 -64
- package/src/render/backend/webgl2/skinScatter.ts +309 -0
- package/src/render/backend/webgpu/buffers.ts +137 -63
- 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 +39 -7
- package/src/render/backend/webgpu/drawCommand.ts +6 -2
- package/src/render/backend/webgpu/flatPass.ts +145 -7
- package/src/render/backend/webgpu/gpuSkinnedCloth.ts +502 -0
- package/src/render/backend/webgpu/motionPass.ts +128 -9
- package/src/render/backend/webgpu/postPass.ts +5 -1
- package/src/render/backend/webgpu/renderer.ts +663 -43
- 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/clothBindingData.ts +89 -0
- package/src/render/clothPlacement.ts +114 -0
- package/src/render/compressedSource.ts +198 -0
- package/src/render/cutoutCaster.ts +5 -0
- package/src/render/cutoutDither.ts +99 -0
- package/src/render/eyeRefraction.ts +72 -0
- package/src/render/glCompressed.ts +86 -0
- package/src/render/hairLobes.ts +188 -0
- package/src/render/lightBudget.ts +25 -0
- package/src/render/mesh.ts +23 -0
- package/src/render/recon/motionVectors.ts +6 -3
- package/src/render/renderQuality.ts +28 -0
- package/src/render/rendererHarness.ts +4 -0
- package/src/render/sceneTarget.ts +21 -0
- package/src/render/shaders/clothBinding.ts +81 -0
- package/src/render/shaders/depth.ts +42 -6
- 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 +36 -2
- 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 +38 -16
- package/src/render/shaders/generated/flat.wgsl.ts +1828 -1742
- package/src/render/shaders/generated/skinBlur.wgsl.ts +56 -0
- package/src/render/shaders/recon/motion.wgsl.ts +102 -0
- package/src/render/shaders/skinBlur.ts +105 -0
- package/src/render/shaders/skinning.ts +31 -4
- 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/surfaceModel.ts +274 -0
- package/src/render/surfaceTexture.ts +123 -63
- package/src/render/vertexDefaults.ts +7 -0
- package/dist/math/exact.d.ts +0 -76
- package/dist/math/exact.js +0 -327
- package/src/math/exact.ts +0 -340
|
@@ -52,6 +52,10 @@ import { createPanelBindGroup, createPanelBindGroupLayout, createPanelCorners, p
|
|
|
52
52
|
import { createGpuTextCube, createTextBindGroup, createTextBindGroupLayout, GpuText, textPipeline, TEXT_FRAG_FIELDS, TEXT_FRAG_SIZE, TEXT_VERT_FIELDS, TEXT_VERT_SIZE, } from './textPass.js';
|
|
53
53
|
import { deviceSnappedCellSize, deviceSnappedOrigin } from '../../textLayout.js';
|
|
54
54
|
import { GpuSurfaceTexture } from './surfaceTexturePass.js';
|
|
55
|
+
import { gpuCompressedFormats } from './compressedUpload.js';
|
|
56
|
+
import { GpuClothBinding, GpuClothParticles, clothStandIn } from './clothTextures.js';
|
|
57
|
+
import { MODEL_PARAM_FLOATS, eyeAxisInWorld, packModel } from '../../surfaceModel.js';
|
|
58
|
+
import { validateClothBinding } from '../../clothBindingData.js';
|
|
55
59
|
import { createSdfTextBindGroup, createSdfTextBindGroupLayout, GpuSdfText, sdfTextPipeline, SDF_TEXT_FRAG_FIELDS, SDF_TEXT_FRAG_SIZE, SDF_TEXT_VERT_FIELDS, SDF_TEXT_VERT_SIZE, } from './sdfTextPass.js';
|
|
56
60
|
import { createFilmBindGroup, createFilmBindGroupLayout, filmPipeline, FILM_FRAG_FIELDS, FILM_FRAG_SIZE, FILM_VERT_FIELDS, FILM_VERT_SIZE, } from './filmPass.js';
|
|
57
61
|
import { reflectionTargetSize } from '../../planarReflectionDraw.js';
|
|
@@ -87,6 +91,7 @@ import { SkinPaletteRing } from './skinPaletteRing.js';
|
|
|
87
91
|
import { DEPTH_FORMAT, FLAT_VERT_FIELDS, FLAT_VERT_SIZE, createFlatBindGroup, createFlatBindGroupLayout, flatFragmentBindings, flatVariant, flatVertexBindings, flatPipeline, flatPipelineAsync, } from './flatPass.js';
|
|
88
92
|
import { DEPTH_FRAG_BINDING, DEPTH_FRAG_SIZE, DEPTH_FRAG_WGSL, DEPTH_PREVIOUS_BINDING, DEPTH_CUTOUT_VERT_FIELDS, DEPTH_CUTOUT_VERT_SIZE, DEPTH_INSTANCED_CUTOUT_VERT_FIELDS, DEPTH_VERT_FIELDS, DEPTH_VERT_SIZE, SHADOW_FORMAT, createDepthBindGroup, createDepthBindGroupLayout, depthPipeline, } from './depthPass.js';
|
|
89
93
|
import { PipelineCache } from './pipelineCache.js';
|
|
94
|
+
import { SKIN_TARGET_FORMAT, SkinScatterPass } from './skinScatterPass.js';
|
|
90
95
|
import { SKY_FIELDS, SKY_UNIFORM_SIZE, createSkyBindGroup, createSkyBindGroupLayout, skyPipeline, } from './skyPass.js';
|
|
91
96
|
import { SCATTER_DEPTH_FIELDS, SCATTER_DEPTH_SIZE, SCATTER_FRAG_FIELDS, SCATTER_FRAG_SIZE, SCATTER_VERT_FIELDS, SCATTER_VERT_SIZE, createGpuScatter, createScatterBindGroup, createScatterBindGroupLayout, createScatterDepthBindGroup, createScatterDepthBindGroupLayout, scatterDepthPipeline, scatterPipeline, } from './scatterPass.js';
|
|
92
97
|
import { WATER_FRAG_FIELDS, WATER_FRAG_SIZE, WATER_VERT_FIELDS, WATER_VERT_SIZE, createGpuWater, createWaterBindGroup, createWaterBindGroupLayout, waterPipeline, } from './waterPass.js';
|
|
@@ -137,6 +142,7 @@ import { clusteredMode, createClusterLightSet, fillClusterLightSet, } from '../.
|
|
|
137
142
|
import { computeAt, createComputeRegistry, } from '../../compute.js';
|
|
138
143
|
import { GpuInstancedBatch } from './instanced.js';
|
|
139
144
|
import { cutoutOf } from '../../cutoutCaster.js';
|
|
145
|
+
import { CUTOUT_RESOLVE_CODE, resolveCutout } from '../../cutoutDither.js';
|
|
140
146
|
import { LightField } from '../../driftLight/lightField.js';
|
|
141
147
|
import { WorldLightField } from '../../driftLight/worldLightField.js';
|
|
142
148
|
import { createDriftLightUniforms, resolveDriftLight } from '../../driftLight/uniforms.js';
|
|
@@ -864,6 +870,12 @@ export class WebGPURenderer {
|
|
|
864
870
|
skinnedGroupsBeside = [];
|
|
865
871
|
skinnedGroupsPalette = [];
|
|
866
872
|
skinnedGroupsDeltas = [];
|
|
873
|
+
/** The cloth views each skinned twin was built with, compared by identity like the palette. */
|
|
874
|
+
skinnedGroupsCloth = [];
|
|
875
|
+
skinnedGroupsClothBinding = [];
|
|
876
|
+
/** The cloth the next skinned draws are placed by, or null. See `setCloth`. */
|
|
877
|
+
cloth = null;
|
|
878
|
+
clothStandCache = null;
|
|
867
879
|
/**
|
|
868
880
|
* Which palette slot the following draws skin by, or -1 for none.
|
|
869
881
|
*
|
|
@@ -1257,6 +1269,16 @@ export class WebGPURenderer {
|
|
|
1257
1269
|
refractSnapshotView = null;
|
|
1258
1270
|
/** Whether this frame has taken it. One snapshot serves every refracting draw. */
|
|
1259
1271
|
refractSnapshotTaken = false;
|
|
1272
|
+
/**
|
|
1273
|
+
* Skin's screen-space scattering, made where the profile asks for it and has a composite to add
|
|
1274
|
+
* into, and null otherwise — which is every profile by default. See `SkinScatterPass`.
|
|
1275
|
+
*/
|
|
1276
|
+
skinScatter;
|
|
1277
|
+
/** The pipelines skin's diffuse half is built for: the blur target's format, the frame's samples. */
|
|
1278
|
+
skinPipelines;
|
|
1279
|
+
/** Whether this frame has spread its skin: once, at its first blended draw or before its end. */
|
|
1280
|
+
skinScattered = false;
|
|
1281
|
+
skinInverseProjection = mat4.create();
|
|
1260
1282
|
sceneColorView = null;
|
|
1261
1283
|
/** The multisampled twin, when the profile asks for samples. Resolved into `sceneColor`. */
|
|
1262
1284
|
sceneColorMsaa = null;
|
|
@@ -1435,6 +1457,17 @@ export class WebGPURenderer {
|
|
|
1435
1457
|
temporalJitterX = 0;
|
|
1436
1458
|
temporalJitterY = 0;
|
|
1437
1459
|
temporalJittering = false;
|
|
1460
|
+
/**
|
|
1461
|
+
* The frame a dithered cutout's pattern is offset by, advanced at `beginFrame` and wrapped at 64.
|
|
1462
|
+
* See `cutoutDither.ts`.
|
|
1463
|
+
*/
|
|
1464
|
+
cutoutFrame = 0;
|
|
1465
|
+
/**
|
|
1466
|
+
* How the current material's cutout edge is drawn this frame, decided when the material is staged
|
|
1467
|
+
* — after `bindMeshPass` has settled whether the frame is temporal — and read by every draw: a
|
|
1468
|
+
* coverage edge is pipeline state here (`|a2c`), and a blended draw tests hard.
|
|
1469
|
+
*/
|
|
1470
|
+
cutoutResolveStaged = 'hard';
|
|
1438
1471
|
/**
|
|
1439
1472
|
* Whether this frame is being reconstructed, settled once in `bindMeshPass`.
|
|
1440
1473
|
*
|
|
@@ -2100,6 +2133,12 @@ export class WebGPURenderer {
|
|
|
2100
2133
|
ormMap = null;
|
|
2101
2134
|
/** The caller's emissive map, or null for the stand-in. See `setMaterial`. */
|
|
2102
2135
|
emissiveMap = null;
|
|
2136
|
+
/** The material's model map, or null for the stand-in. See `setMaterial` and `surfaceModel.ts`. */
|
|
2137
|
+
modelMap = null;
|
|
2138
|
+
/** The material's shading model, which picks the lit pipeline: null the standard. */
|
|
2139
|
+
materialModel = null;
|
|
2140
|
+
/** Its numbers, `uModelParams`, packed by `packModel`. */
|
|
2141
|
+
modelParams = new Float32Array(MODEL_PARAM_FLOATS);
|
|
2103
2142
|
/**
|
|
2104
2143
|
* The flat group for a material, keyed on **all four** of its maps.
|
|
2105
2144
|
*
|
|
@@ -2143,11 +2182,21 @@ export class WebGPURenderer {
|
|
|
2143
2182
|
/** Whether the bound material has a map, for the warning a mesh with no coordinates earns. */
|
|
2144
2183
|
materialHasMaps = false;
|
|
2145
2184
|
createSurfaceTexture(source, options = {}) {
|
|
2146
|
-
return new GpuSurfaceTexture(this.surface.device, this.pipelines, source, options);
|
|
2185
|
+
return new GpuSurfaceTexture(this.surface.device, this.pipelines, source, options, this.compressedFormats);
|
|
2186
|
+
}
|
|
2187
|
+
compressedCache = null;
|
|
2188
|
+
/**
|
|
2189
|
+
* The BC formats this device takes as blocks: every one where the device carries
|
|
2190
|
+
* `texture-compression-bc`, which `select.ts` asks for wherever the adapter offers it, and none
|
|
2191
|
+
* where it does not. See `Renderer.compressedFormats`.
|
|
2192
|
+
*/
|
|
2193
|
+
get compressedFormats() {
|
|
2194
|
+
this.compressedCache ??= gpuCompressedFormats(this.surface.device);
|
|
2195
|
+
return this.compressedCache;
|
|
2147
2196
|
}
|
|
2148
2197
|
/** Several images of one size as an array's layers. See `RendererApi.createSurfaceTextureArray`. */
|
|
2149
2198
|
createSurfaceTextureArray(sources, options = {}) {
|
|
2150
|
-
return new GpuSurfaceTexture(this.surface.device, this.pipelines, sources, options);
|
|
2199
|
+
return new GpuSurfaceTexture(this.surface.device, this.pipelines, sources, options, this.compressedFormats);
|
|
2151
2200
|
}
|
|
2152
2201
|
/**
|
|
2153
2202
|
* GPU textures an `updateSurfaceTexture` replaced, destroyed when the next frame begins.
|
|
@@ -2198,6 +2247,8 @@ export class WebGPURenderer {
|
|
|
2198
2247
|
this.ormMap = null;
|
|
2199
2248
|
if (this.emissiveMap === texture)
|
|
2200
2249
|
this.emissiveMap = null;
|
|
2250
|
+
if (this.modelMap === texture)
|
|
2251
|
+
this.modelMap = null;
|
|
2201
2252
|
this.forgetBindingsOf(texture);
|
|
2202
2253
|
texture.dispose();
|
|
2203
2254
|
}
|
|
@@ -2222,7 +2273,7 @@ export class WebGPURenderer {
|
|
|
2222
2273
|
this.albedo = null;
|
|
2223
2274
|
this.blankAlbedoBindGroup = this.buildFlatBindGroup();
|
|
2224
2275
|
this.albedo = held;
|
|
2225
|
-
this.bindGroup = this.flatGroupForMaps(held, this.normalMap, this.ormMap, this.emissiveMap);
|
|
2276
|
+
this.bindGroup = this.flatGroupForMaps(held, this.normalMap, this.ormMap, this.emissiveMap, this.modelMap);
|
|
2226
2277
|
}
|
|
2227
2278
|
/**
|
|
2228
2279
|
* Choose the surface texture the following `drawMesh` calls sample, or null for none.
|
|
@@ -2250,6 +2301,104 @@ export class WebGPURenderer {
|
|
|
2250
2301
|
one. See `SkinPaletteRing`; a full ring declines the draw instead of mixing two rigs. */
|
|
2251
2302
|
this.skinPaletteSlot = this.skinPalettes.take(this.surface.device, palette) ?? -1;
|
|
2252
2303
|
}
|
|
2304
|
+
/**
|
|
2305
|
+
* A mesh's cloth binding. See `RendererApi.createClothBinding`. The pipelines that draw it bound
|
|
2306
|
+
* are warmed here, every way this mesh can be drawn, so none compiles inside the first frame
|
|
2307
|
+
* that draws a garment.
|
|
2308
|
+
*/
|
|
2309
|
+
createClothBinding(mesh, data) {
|
|
2310
|
+
if (!mesh.isSkinned) {
|
|
2311
|
+
throw new Error('createClothBinding: this mesh has no rig, and a binding blends each vertex between its ' +
|
|
2312
|
+
'skinning and the cloth');
|
|
2313
|
+
}
|
|
2314
|
+
validateClothBinding(data, mesh.vertexCount);
|
|
2315
|
+
const base = mesh.key ?? 'flat:s0:u0';
|
|
2316
|
+
const present = this.meshPresent.get(base);
|
|
2317
|
+
if (present !== undefined) {
|
|
2318
|
+
const skin = mesh.isSkinnedEight ? '|skin8' : '|skin';
|
|
2319
|
+
for (const morphed of mesh.morph !== null ? [false, true] : [false]) {
|
|
2320
|
+
for (const translucent of [false, true]) {
|
|
2321
|
+
void flatPipelineAsync(this.targetPipelines(), this.surface.device, this.flatLayoutFor(true, morphed), this.variant, `${base}${morphed ? '|morph' : ''}${skin}|cloth${translucent ? '|blend' : ''}`, present, translucent, true, morphed, mesh.isSkinnedEight, true);
|
|
2322
|
+
}
|
|
2323
|
+
}
|
|
2324
|
+
}
|
|
2325
|
+
return new GpuClothBinding(this.surface.device, data);
|
|
2326
|
+
}
|
|
2327
|
+
/** One character's particles. See `RendererApi.createClothParticles`. */
|
|
2328
|
+
createClothParticles(count) {
|
|
2329
|
+
return new GpuClothParticles(this.surface.device, count);
|
|
2330
|
+
}
|
|
2331
|
+
/**
|
|
2332
|
+
* This frame's particles. **Once a frame, before the character's draws**: a texture write lands
|
|
2333
|
+
* ahead of every draw of the frame, so a second update between two draws would reach both.
|
|
2334
|
+
*/
|
|
2335
|
+
updateClothParticles(particles, positions) {
|
|
2336
|
+
if (this.surface.lost)
|
|
2337
|
+
return;
|
|
2338
|
+
/* A write between frames is the next frame's, as a rewritten mesh's is. See `changeFrames.ts`. */
|
|
2339
|
+
particles.update(this.surface.device, positions, frameOfChange(this.moverFrame, this.inFrame));
|
|
2340
|
+
}
|
|
2341
|
+
/** Place the following skinned draws by a cloth, or by none. See `RendererApi.setCloth`. */
|
|
2342
|
+
setCloth(binding, particles = null) {
|
|
2343
|
+
if (binding === null || particles === null) {
|
|
2344
|
+
this.cloth = null;
|
|
2345
|
+
return;
|
|
2346
|
+
}
|
|
2347
|
+
if (binding.particles !== particles.count) {
|
|
2348
|
+
throw new Error(`setCloth: the binding names ${binding.particles} particles and these are ${particles.count}`);
|
|
2349
|
+
}
|
|
2350
|
+
this.cloth = { binding, particles };
|
|
2351
|
+
}
|
|
2352
|
+
/** Retired rather than destroyed: a draw recorded earlier in this frame may still read them. */
|
|
2353
|
+
disposeClothBinding(binding) {
|
|
2354
|
+
this.retiredTextures.push(...binding.textures);
|
|
2355
|
+
}
|
|
2356
|
+
disposeClothParticles(particles) {
|
|
2357
|
+
this.retiredTextures.push(...particles.textures);
|
|
2358
|
+
}
|
|
2359
|
+
/**
|
|
2360
|
+
* The device and the frame's bookkeeping, for compute a consumer opts into by importing it —
|
|
2361
|
+
* `createSkinnedCloth` — rather than for anything this renderer runs itself. See `computeHost.ts`.
|
|
2362
|
+
*/
|
|
2363
|
+
computeHost() {
|
|
2364
|
+
return this.computeHostCache;
|
|
2365
|
+
}
|
|
2366
|
+
computeHostCache = {
|
|
2367
|
+
device: () => this.surface.device,
|
|
2368
|
+
lost: () => this.surface.lost,
|
|
2369
|
+
changeFrame: () => frameOfChange(this.moverFrame, this.inFrame),
|
|
2370
|
+
retire: (textures) => {
|
|
2371
|
+
this.retiredTextures.push(...textures);
|
|
2372
|
+
},
|
|
2373
|
+
};
|
|
2374
|
+
/**
|
|
2375
|
+
* An eye's axis in the world for this draw, into the open material's \`uModelParams\`, and a new
|
|
2376
|
+
* material slot when it moved: the axis is per draw, and a slot already taken by an earlier draw
|
|
2377
|
+
* of the same eye must not be written under it. See \`eyeAxisInWorld\`.
|
|
2378
|
+
*/
|
|
2379
|
+
writeEyeAxis(model, skinned) {
|
|
2380
|
+
const eye = this.materialModel;
|
|
2381
|
+
if (eye?.kind !== 'eye')
|
|
2382
|
+
return;
|
|
2383
|
+
const at = this.materialField('uModelParams') + 4;
|
|
2384
|
+
const f = this.perFrameFloats;
|
|
2385
|
+
const before0 = f[at];
|
|
2386
|
+
const before1 = f[at + 1];
|
|
2387
|
+
const before2 = f[at + 2];
|
|
2388
|
+
eyeAxisInWorld(eye, model, skinned ? this.skinPaletteData : null, f, at);
|
|
2389
|
+
if (f[at] !== before0 || f[at + 1] !== before1 || f[at + 2] !== before2)
|
|
2390
|
+
this.materials.dirty();
|
|
2391
|
+
}
|
|
2392
|
+
/** The views the draw's cloth binds, or the stand-in's where no cloth is set. */
|
|
2393
|
+
clothViews() {
|
|
2394
|
+
const cloth = this.cloth;
|
|
2395
|
+
return cloth === null ? this.clothStand() : clothViewsOf(cloth.binding, cloth.particles);
|
|
2396
|
+
}
|
|
2397
|
+
/** The one-texel stand-in a skinned group binds where there is no cloth, made on first need. */
|
|
2398
|
+
clothStand() {
|
|
2399
|
+
this.clothStandCache ??= clothStandIn(this.surface.device);
|
|
2400
|
+
return this.clothStandCache.views;
|
|
2401
|
+
}
|
|
2253
2402
|
/**
|
|
2254
2403
|
* Choose the morph weights the following `drawMesh` calls deform by, or null for none.
|
|
2255
2404
|
*
|
|
@@ -2274,14 +2423,32 @@ export class WebGPURenderer {
|
|
|
2274
2423
|
const normal = (material?.normal ?? null);
|
|
2275
2424
|
const orm = (material?.orm ?? null);
|
|
2276
2425
|
const emissiveMap = (material?.emissive ?? null);
|
|
2426
|
+
const modelMap = (material?.modelMap ?? null);
|
|
2427
|
+
this.materialModel = material?.model ?? null;
|
|
2428
|
+
/* What the blur spreads this skin by: the last material to name a profile says. */
|
|
2429
|
+
const model = material?.model;
|
|
2430
|
+
if (model?.kind === 'skin' && this.skinScatter !== null) {
|
|
2431
|
+
this.skinScatter.setProfile(model.profile, model.scatterColor[0] * model.radius, model.scatterColor[1] * model.radius, model.scatterColor[2] * model.radius);
|
|
2432
|
+
}
|
|
2277
2433
|
this.material((f, i) => {
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
i[
|
|
2434
|
+
/* The material's four switches, one row: albedo, ORM map, emissive map, two-sided. */
|
|
2435
|
+
const flags = this.materialField('uMaterialFlags');
|
|
2436
|
+
i[flags] = albedo === null ? 0 : 1;
|
|
2437
|
+
/* The cutoff, how this frame resolves a dithered edge, and the frame its pattern moves with. */
|
|
2438
|
+
const cutoff = albedo === null ? 0 : (material?.cutout ?? 0);
|
|
2439
|
+
this.cutoutResolveStaged =
|
|
2440
|
+
cutoff > 0
|
|
2441
|
+
? resolveCutout(material?.cutoutMode ?? 'hard', this.reconstructing || this.temporalJittering, this.samples)
|
|
2442
|
+
: 'hard';
|
|
2443
|
+
const cut = this.materialField('uCutout');
|
|
2444
|
+
f[cut] = cutoff;
|
|
2445
|
+
f[cut + 1] = CUTOUT_RESOLVE_CODE[this.cutoutResolveStaged];
|
|
2446
|
+
f[cut + 2] = this.cutoutFrame;
|
|
2447
|
+
i[flags + 3] = material?.doubleSided === true ? 1 : 0;
|
|
2281
2448
|
/* 0 is no map, and gates the whole block in the shader. See `uNormalStrength`. */
|
|
2282
2449
|
f[this.materialField('uNormalStrength')] =
|
|
2283
2450
|
normal === null ? 0 : (material?.normalStrength ?? 1);
|
|
2284
|
-
i[
|
|
2451
|
+
i[flags + 1] = orm === null ? 0 : 1;
|
|
2285
2452
|
/*
|
|
2286
2453
|
* Component-aligned with the map — r occlusion, g roughness, b metallic — so this order is
|
|
2287
2454
|
* the channel order rather than the field order of `SurfaceMaterial`. Written component by
|
|
@@ -2292,7 +2459,7 @@ export class WebGPURenderer {
|
|
|
2292
2459
|
f[scale] = material?.occlusionStrength ?? 1;
|
|
2293
2460
|
f[scale + 1] = material?.roughnessScale ?? 1;
|
|
2294
2461
|
f[scale + 2] = material?.metallicScale ?? 1;
|
|
2295
|
-
i[
|
|
2462
|
+
i[flags + 2] = emissiveMap === null ? 0 : 1;
|
|
2296
2463
|
/*
|
|
2297
2464
|
* One rather than zero on every component, because this multiplies rather than adds: an
|
|
2298
2465
|
* unwritten float is zero, and zero here would switch off every glow the mesh's own vertex
|
|
@@ -2303,6 +2470,9 @@ export class WebGPURenderer {
|
|
|
2303
2470
|
f[emissive] = asked?.[0] ?? 1;
|
|
2304
2471
|
f[emissive + 1] = asked?.[1] ?? 1;
|
|
2305
2472
|
f[emissive + 2] = asked?.[2] ?? 1;
|
|
2473
|
+
/* The model's numbers and whether its map is bound. See `packModel`. */
|
|
2474
|
+
packModel(material?.model ?? null, modelMap !== null, this.modelParams);
|
|
2475
|
+
f.set(this.modelParams, this.materialField('uModelParams'));
|
|
2306
2476
|
});
|
|
2307
2477
|
/*
|
|
2308
2478
|
* **One lookup for the whole material, and nothing thrown away.**
|
|
@@ -2316,14 +2486,16 @@ export class WebGPURenderer {
|
|
|
2316
2486
|
if (this.albedo === albedo &&
|
|
2317
2487
|
this.normalMap === normal &&
|
|
2318
2488
|
this.ormMap === orm &&
|
|
2319
|
-
this.emissiveMap === emissiveMap
|
|
2489
|
+
this.emissiveMap === emissiveMap &&
|
|
2490
|
+
this.modelMap === modelMap) {
|
|
2320
2491
|
return;
|
|
2321
2492
|
}
|
|
2322
2493
|
this.albedo = albedo;
|
|
2323
2494
|
this.normalMap = normal;
|
|
2324
2495
|
this.ormMap = orm;
|
|
2325
2496
|
this.emissiveMap = emissiveMap;
|
|
2326
|
-
this.
|
|
2497
|
+
this.modelMap = modelMap;
|
|
2498
|
+
this.bindGroup = this.flatGroupForMaps(albedo, normal, orm, emissiveMap, modelMap);
|
|
2327
2499
|
}
|
|
2328
2500
|
/**
|
|
2329
2501
|
* @deprecated Use `setMaterial`. See `renderer.ts` for why this stays: 56 call sites across four
|
|
@@ -2345,8 +2517,14 @@ export class WebGPURenderer {
|
|
|
2345
2517
|
* asking. Passing them in as well is deliberate: it is what makes the key and the group agree by
|
|
2346
2518
|
* construction rather than by the caller having remembered.
|
|
2347
2519
|
*/
|
|
2348
|
-
flatGroupForMaps(albedo, normal, orm, emissive
|
|
2349
|
-
|
|
2520
|
+
flatGroupForMaps(albedo, normal, orm, emissive,
|
|
2521
|
+
/** The model map, a fifth level of the same cache. See `flatBindGroups`. */
|
|
2522
|
+
modelMap) {
|
|
2523
|
+
if (albedo === null &&
|
|
2524
|
+
normal === null &&
|
|
2525
|
+
orm === null &&
|
|
2526
|
+
emissive === null &&
|
|
2527
|
+
modelMap === null) {
|
|
2350
2528
|
return this.blankAlbedoBindGroup;
|
|
2351
2529
|
}
|
|
2352
2530
|
let byNormal = this.flatBindGroups.get(albedo);
|
|
@@ -2364,12 +2542,17 @@ export class WebGPURenderer {
|
|
|
2364
2542
|
byEmissive = new Map();
|
|
2365
2543
|
byOrm.set(orm, byEmissive);
|
|
2366
2544
|
}
|
|
2367
|
-
|
|
2545
|
+
let byModel = byEmissive.get(emissive);
|
|
2546
|
+
if (byModel === undefined) {
|
|
2547
|
+
byModel = new Map();
|
|
2548
|
+
byEmissive.set(emissive, byModel);
|
|
2549
|
+
}
|
|
2550
|
+
const existing = byModel.get(modelMap);
|
|
2368
2551
|
if (existing !== undefined)
|
|
2369
2552
|
return existing;
|
|
2370
2553
|
const built = this.buildFlatBindGroup();
|
|
2371
2554
|
this.bindGroupBudget.ask();
|
|
2372
|
-
|
|
2555
|
+
byModel.set(modelMap, built);
|
|
2373
2556
|
return built;
|
|
2374
2557
|
}
|
|
2375
2558
|
/* -- Material state ------------------------------------------------------------------ */
|
|
@@ -4265,6 +4448,8 @@ export class WebGPURenderer {
|
|
|
4265
4448
|
shadowSkinnedGroups = [];
|
|
4266
4449
|
shadowSkinnedGroupsBeside = [];
|
|
4267
4450
|
shadowSkinnedGroupsPalette = [];
|
|
4451
|
+
shadowSkinnedGroupsCloth = [];
|
|
4452
|
+
shadowSkinnedGroupsClothBinding = [];
|
|
4268
4453
|
/** Held from construction so the skinned group can be rebuilt without them being threaded. */
|
|
4269
4454
|
shadowBlankView = null;
|
|
4270
4455
|
shadowBlankSampler = null;
|
|
@@ -4386,6 +4571,7 @@ export class WebGPURenderer {
|
|
|
4386
4571
|
this.shadowDraws.writeFloat(slot, fields.uUvOffset.offset, cutout.uOffset);
|
|
4387
4572
|
this.shadowDraws.writeFloat(slot, fields.uUvOffset.offset + 4, cutout.vOffset);
|
|
4388
4573
|
this.shadowDraws.writeFloat(slot, fields.uAlphaCutout.offset, cutout.cutoff);
|
|
4574
|
+
this.shadowDraws.writeFloat(slot, fields.uAlphaCutout.offset + 4, cutout.dithered);
|
|
4389
4575
|
}
|
|
4390
4576
|
shadowGroup() {
|
|
4391
4577
|
return this.shadowLayerIsPeel && this.shadowPeelBindGroup !== null
|
|
@@ -4399,14 +4585,18 @@ export class WebGPURenderer {
|
|
|
4399
4585
|
* second independently fading occluder can be recorded, and a character is not one — it is the
|
|
4400
4586
|
* thing in front. Serving it would mean a second cached group for a case nothing produces.
|
|
4401
4587
|
*/
|
|
4402
|
-
shadowSkinnedGroup(palette, at) {
|
|
4588
|
+
shadowSkinnedGroup(palette, at, cloth) {
|
|
4403
4589
|
const beside = this.shadowBindGroup;
|
|
4404
4590
|
if ((this.shadowSkinnedGroups[at] ?? null) === null ||
|
|
4405
4591
|
this.shadowSkinnedGroupsBeside[at] !== beside ||
|
|
4406
|
-
this.shadowSkinnedGroupsPalette[at] !== palette
|
|
4407
|
-
this.
|
|
4592
|
+
this.shadowSkinnedGroupsPalette[at] !== palette ||
|
|
4593
|
+
this.shadowSkinnedGroupsCloth[at] !== cloth.particles ||
|
|
4594
|
+
this.shadowSkinnedGroupsClothBinding[at] !== cloth.binding) {
|
|
4595
|
+
this.shadowSkinnedGroups[at] = createDepthBindGroup(this.surface.device, this.shadowSkinnedLayout, this.shadowDraws.buffer, this.shadowPassUniforms, this.shadowBlankView, this.shadowBlankSampler, palette, null, cloth);
|
|
4408
4596
|
this.shadowSkinnedGroupsBeside[at] = beside;
|
|
4409
4597
|
this.shadowSkinnedGroupsPalette[at] = palette;
|
|
4598
|
+
this.shadowSkinnedGroupsCloth[at] = cloth.particles;
|
|
4599
|
+
this.shadowSkinnedGroupsClothBinding[at] = cloth.binding;
|
|
4410
4600
|
}
|
|
4411
4601
|
return this.shadowSkinnedGroups[at];
|
|
4412
4602
|
}
|
|
@@ -4445,6 +4635,8 @@ export class WebGPURenderer {
|
|
|
4445
4635
|
void this.overlayPipelines.enable(feature);
|
|
4446
4636
|
if (this.latePipelines !== this.pipelines)
|
|
4447
4637
|
void this.latePipelines.enable(feature);
|
|
4638
|
+
if (this.skinPipelines !== null)
|
|
4639
|
+
void this.skinPipelines.enable(feature);
|
|
4448
4640
|
}
|
|
4449
4641
|
/** One resolution every glass caster is read through, so recording one allocates nothing. */
|
|
4450
4642
|
glassCasterScratch = {
|
|
@@ -4535,7 +4727,7 @@ export class WebGPURenderer {
|
|
|
4535
4727
|
pass.setIndexBuffer(geometry.indexBuffer, 'uint32');
|
|
4536
4728
|
pass.drawIndexed(geometry.indexCount, count);
|
|
4537
4729
|
},
|
|
4538
|
-
skinnedMesh: (mesh, model, palette, material) => {
|
|
4730
|
+
skinnedMesh: (mesh, model, palette, material, cloth) => {
|
|
4539
4731
|
if (!this.drawingGlassDepth && !castsDepth(material)) {
|
|
4540
4732
|
if (this.quality.glassShadows !== 'off' && glassOf(material, this.glassCasterScratch)) {
|
|
4541
4733
|
this.glassCasters.recordSkinned(mesh, model, palette, material, this.glassCasterScratch);
|
|
@@ -4565,8 +4757,13 @@ export class WebGPURenderer {
|
|
|
4565
4757
|
const geometry = mesh;
|
|
4566
4758
|
if (geometry.vertexBuffers === undefined || geometry.indexCount === 0)
|
|
4567
4759
|
return;
|
|
4568
|
-
|
|
4569
|
-
|
|
4760
|
+
/* A garment casts placed by its cloth, as it draws; `|cloth` keys the pipeline that does. */
|
|
4761
|
+
const clothed = cloth !== undefined;
|
|
4762
|
+
const clothViews = clothed
|
|
4763
|
+
? clothViewsOf(cloth.binding, cloth.particles)
|
|
4764
|
+
: this.clothStand();
|
|
4765
|
+
pass.setPipeline(depthPipeline(this.pipelines, this.surface.device, this.shadowSkinnedLayout, `${geometry.isSkinnedEight ? 'depth-skinned8' : 'depth-skinned'}${clothed ? '|cloth' : ''}|${this.depthCullMode}|${geometry.key ?? ''}`, this.presentOf(geometry), this.depthCullMode, true, false, false, geometry.isSkinnedEight, clothed));
|
|
4766
|
+
pass.setBindGroup(0, this.shadowSkinnedGroup(view, paletteSlot, clothViews), [slot]);
|
|
4570
4767
|
for (let index = 0; index < geometry.vertexBuffers.length; index++) {
|
|
4571
4768
|
pass.setVertexBuffer(index, geometry.vertexBuffers[index]);
|
|
4572
4769
|
}
|
|
@@ -4899,6 +5096,18 @@ export class WebGPURenderer {
|
|
|
4899
5096
|
this.pipelines.format === RECON_HISTORY_FORMAT && this.samples === 1
|
|
4900
5097
|
? this.pipelines
|
|
4901
5098
|
: new PipelineCache(device, RECON_HISTORY_FORMAT, 1, glassShadows, quality.clusteredLights);
|
|
5099
|
+
/*
|
|
5100
|
+
* Screen-space skin needs the composite: linear light to add into, and the depth it resolves.
|
|
5101
|
+
* Without one it is the pre-integrated fit alone, which is the default anyway, and said once.
|
|
5102
|
+
*/
|
|
5103
|
+
const skinScreen = quality.skinScattering === 'screen-space' && quality.screenEffects;
|
|
5104
|
+
if (quality.skinScattering === 'screen-space' && !quality.screenEffects) {
|
|
5105
|
+
console.warn("WebGPU: skinScattering 'screen-space' needs screenEffects to add into; skin is pre-integrated");
|
|
5106
|
+
}
|
|
5107
|
+
this.skinScatter = skinScreen ? new SkinScatterPass(device) : null;
|
|
5108
|
+
this.skinPipelines = skinScreen
|
|
5109
|
+
? new PipelineCache(device, SKIN_TARGET_FORMAT, this.samples, glassShadows, quality.clusteredLights)
|
|
5110
|
+
: null;
|
|
4902
5111
|
this.issueBound = this.issueCommand.bind(this);
|
|
4903
5112
|
this.pipelineTargets =
|
|
4904
5113
|
this.overlayPipelines === this.pipelines
|
|
@@ -4953,7 +5162,7 @@ export class WebGPURenderer {
|
|
|
4953
5162
|
*/
|
|
4954
5163
|
this.perFrame = new UniformRing(device, this.fragment.uniformSize, this.quality.materialChangesPerFrame, USAGE_UNIFORM_DST, 'flat.fragRing', this.materialBudget);
|
|
4955
5164
|
/*
|
|
4956
|
-
* A one-pixel stand-in for the albedo binding.
|
|
5165
|
+
* A one-pixel stand-in for the albedo binding. The albedo's flag is zero for untextured
|
|
4957
5166
|
* geometry, which is nearly all of it, so the shader never samples this — but WebGPU
|
|
4958
5167
|
* requires every declared binding to be filled. WebGL2 keeps an `emptyTexture2D` for
|
|
4959
5168
|
* exactly the same reason.
|
|
@@ -5157,6 +5366,13 @@ export class WebGPURenderer {
|
|
|
5157
5366
|
? { view: blankSurfaceView, sampler: blankSampler }
|
|
5158
5367
|
: { view: map.view, sampler: map.sampler };
|
|
5159
5368
|
}
|
|
5369
|
+
/* The material's model map, or the stand-in, for the reason `uNormalMap` gives above. */
|
|
5370
|
+
if (name === 'uModelMap') {
|
|
5371
|
+
const map = this.modelMap;
|
|
5372
|
+
return map === null
|
|
5373
|
+
? { view: blankSurfaceView, sampler: blankSampler }
|
|
5374
|
+
: { view: map.view, sampler: map.sampler };
|
|
5375
|
+
}
|
|
5160
5376
|
if (name === 'uOrmMap') {
|
|
5161
5377
|
const map = this.ormMap;
|
|
5162
5378
|
return map === null
|
|
@@ -5732,6 +5948,8 @@ export class WebGPURenderer {
|
|
|
5732
5948
|
}
|
|
5733
5949
|
/* Not a layout entry of its own — it widens `uvs` — so read from the key beside them. */
|
|
5734
5950
|
present['layers'] = key.includes(':layers');
|
|
5951
|
+
/* Nor is a dynamic mesh's split, which moves positions and normals into buffers of their own. */
|
|
5952
|
+
present['dynamic'] = key.includes(':dynamic');
|
|
5735
5953
|
return present;
|
|
5736
5954
|
}
|
|
5737
5955
|
/**
|
|
@@ -5744,6 +5962,7 @@ export class WebGPURenderer {
|
|
|
5744
5962
|
*/
|
|
5745
5963
|
beginFrame(clearColor) {
|
|
5746
5964
|
this.cullFrame += 1;
|
|
5965
|
+
this.cutoutFrame = (this.cutoutFrame + 1) % 64;
|
|
5747
5966
|
/* Whatever an update replaced during the last frame; nothing recorded can read it now. */
|
|
5748
5967
|
for (const texture of this.retiredTextures)
|
|
5749
5968
|
texture.destroy();
|
|
@@ -5807,6 +6026,8 @@ export class WebGPURenderer {
|
|
|
5807
6026
|
this.volumeDepthTaken = false;
|
|
5808
6027
|
/* Last frame's snapshot is last frame's scene. */
|
|
5809
6028
|
this.refractSnapshotTaken = false;
|
|
6029
|
+
this.skinScattered = false;
|
|
6030
|
+
this.skinScatter?.reset();
|
|
5810
6031
|
this.plumeVerts.reset();
|
|
5811
6032
|
this.plumeFrags.reset();
|
|
5812
6033
|
this.waterVerts.reset();
|
|
@@ -5983,7 +6204,7 @@ export class WebGPURenderer {
|
|
|
5983
6204
|
* neither the pipeline set nor the key depends on whether the geometry lands in one call or
|
|
5984
6205
|
* over six frames.
|
|
5985
6206
|
*/
|
|
5986
|
-
warmFlatPipelines(data) {
|
|
6207
|
+
warmFlatPipelines(data, dynamic) {
|
|
5987
6208
|
/*
|
|
5988
6209
|
* **Every optional attribute, not the two that come to mind.** `present` decides where
|
|
5989
6210
|
* each attribute sits in the interleaved buffer, and `createGpuMesh` decides the same
|
|
@@ -6010,6 +6231,15 @@ export class WebGPURenderer {
|
|
|
6010
6231
|
present['layers'] = true;
|
|
6011
6232
|
key += ':layers';
|
|
6012
6233
|
}
|
|
6234
|
+
/*
|
|
6235
|
+
* A dynamic mesh keeps its positions and normals in buffers of their own (`GpuMesh.update`), so
|
|
6236
|
+
* it binds four buffers where a static one binds two: a different layout, and a key that says
|
|
6237
|
+
* so, or the two would share a pipeline whose buffers one of them does not have.
|
|
6238
|
+
*/
|
|
6239
|
+
if (dynamic) {
|
|
6240
|
+
present['dynamic'] = true;
|
|
6241
|
+
key += ':dynamic';
|
|
6242
|
+
}
|
|
6013
6243
|
const fullKey = `${this.variant}|${key}`;
|
|
6014
6244
|
/*
|
|
6015
6245
|
* **For every target this mesh can land on, which is two when the overlay is its own.**
|
|
@@ -6065,14 +6295,16 @@ export class WebGPURenderer {
|
|
|
6065
6295
|
* guessing which the scene will use.
|
|
6066
6296
|
*/
|
|
6067
6297
|
const skinnable = data.joints !== undefined;
|
|
6298
|
+
/* An eight-influence rig takes its own skinned pipelines, keyed `|skin8`. See skinning.ts. */
|
|
6299
|
+
const eight = skinnable && data.joints2 !== undefined;
|
|
6068
6300
|
const morphable = data.morphTargets !== undefined;
|
|
6069
6301
|
for (const wantSkin of skinnable ? [false, true] : [false]) {
|
|
6070
6302
|
for (const wantMorph of morphable ? [false, true] : [false]) {
|
|
6071
6303
|
if (!wantSkin && !wantMorph)
|
|
6072
6304
|
continue;
|
|
6073
|
-
const suffix = `${wantMorph ? '|morph' : ''}${wantSkin ? '|skin' : ''}`;
|
|
6305
|
+
const suffix = `${wantMorph ? '|morph' : ''}${wantSkin ? (eight ? '|skin8' : '|skin') : ''}`;
|
|
6074
6306
|
for (const translucent of [false, true]) {
|
|
6075
|
-
void flatPipelineAsync(pipelines, this.surface.device, this.flatLayoutFor(wantSkin, wantMorph), this.variant, `${fullKey}${suffix}${translucent ? '|blend' : ''}`, present, translucent, wantSkin, wantMorph);
|
|
6307
|
+
void flatPipelineAsync(pipelines, this.surface.device, this.flatLayoutFor(wantSkin, wantMorph), this.variant, `${fullKey}${suffix}${translucent ? '|blend' : ''}`, present, translucent, wantSkin, wantMorph, wantSkin && eight);
|
|
6076
6308
|
}
|
|
6077
6309
|
}
|
|
6078
6310
|
}
|
|
@@ -6093,7 +6325,7 @@ export class WebGPURenderer {
|
|
|
6093
6325
|
return keyed;
|
|
6094
6326
|
}
|
|
6095
6327
|
createMesh(data, options = {}) {
|
|
6096
|
-
const fullKey = this.warmFlatPipelines(data);
|
|
6328
|
+
const fullKey = this.warmFlatPipelines(data, options.dynamic === true);
|
|
6097
6329
|
return this.keyed(createGpuMesh(this.surface.device, data, options.dynamic === true), fullKey);
|
|
6098
6330
|
}
|
|
6099
6331
|
/**
|
|
@@ -6106,7 +6338,7 @@ export class WebGPURenderer {
|
|
|
6106
6338
|
* can do nothing about. Done with `complete` still false is the signal that it was abandoned.
|
|
6107
6339
|
*/
|
|
6108
6340
|
createMeshIncremental(data, options = {}) {
|
|
6109
|
-
const fullKey = this.warmFlatPipelines(data);
|
|
6341
|
+
const fullKey = this.warmFlatPipelines(data, options.dynamic === true);
|
|
6110
6342
|
const { mesh, upload } = createGpuMeshIncremental(this.surface.device, data, options.dynamic === true);
|
|
6111
6343
|
const surface = this.surface;
|
|
6112
6344
|
return {
|
|
@@ -6123,18 +6355,18 @@ export class WebGPURenderer {
|
|
|
6123
6355
|
/**
|
|
6124
6356
|
* Rewrite a mesh's positions, and its normals where the caller has them.
|
|
6125
6357
|
*
|
|
6126
|
-
* Refused in words on a mesh that did not declare itself dynamic, because
|
|
6127
|
-
*
|
|
6128
|
-
*
|
|
6358
|
+
* Refused in words on a mesh that did not declare itself dynamic, because what makes an update
|
|
6359
|
+
* cheap on this backend — positions and normals in buffers of their own — is a layout chosen at
|
|
6360
|
+
* creation. See `GpuMesh.update`.
|
|
6129
6361
|
*/
|
|
6130
6362
|
updateMesh(mesh, positions, normals) {
|
|
6131
6363
|
if (mesh.update === null) {
|
|
6132
|
-
throw new Error('this mesh was not created with { dynamic: true }, so
|
|
6133
|
-
'
|
|
6134
|
-
'
|
|
6364
|
+
throw new Error('this mesh was not created with { dynamic: true }, so its positions share one buffer with ' +
|
|
6365
|
+
'every other attribute and cannot be rewritten alone. Say so at `createMesh` — the flag ' +
|
|
6366
|
+
'decides the layout, and what a buffer is hinted as.');
|
|
6135
6367
|
}
|
|
6136
6368
|
/*
|
|
6137
|
-
* **The
|
|
6369
|
+
* **The positions before this rewrite are last frame's**, kept only for a reconstruction and only
|
|
6138
6370
|
* on the first rewrite of a frame — a second must not replace where the vertices were last
|
|
6139
6371
|
* frame with where they were a moment ago. A rewrite between frames is the next frame's. See
|
|
6140
6372
|
* `changeFrames.ts`.
|
|
@@ -6268,7 +6500,11 @@ export class WebGPURenderer {
|
|
|
6268
6500
|
i[at('uLightingEnabled')] = 1;
|
|
6269
6501
|
i[at('uFogEnabled')] = FOG_RECEDE;
|
|
6270
6502
|
this.surfaceFog = FOG_RECEDE;
|
|
6271
|
-
|
|
6503
|
+
/* Every material switch off — albedo, ORM map, emissive map, two-sided — as WebGL2 resets them. */
|
|
6504
|
+
i.fill(0, at('uMaterialFlags'), at('uMaterialFlags') + 4);
|
|
6505
|
+
f[at('uCutout')] = 0;
|
|
6506
|
+
f[at('uCutout') + 1] = 0;
|
|
6507
|
+
this.cutoutResolveStaged = 'hard';
|
|
6272
6508
|
this.materialHasMaps = false;
|
|
6273
6509
|
/*
|
|
6274
6510
|
* **The camera medium, which this pass did not upload until 2026-08-29.**
|
|
@@ -6399,7 +6635,7 @@ export class WebGPURenderer {
|
|
|
6399
6635
|
/*
|
|
6400
6636
|
* The ORM scales, whose base is 1 and not 0 — the value an unwritten uniform would hold.
|
|
6401
6637
|
*
|
|
6402
|
-
*
|
|
6638
|
+
* The ORM map's flag is zeroed with the others above, and zero is exactly "no map".
|
|
6403
6639
|
* These three are the opposite case, the one this block's own header is about: a scale of
|
|
6404
6640
|
* zero is a real and wrong value, so leaving them silent would not switch the map off, it
|
|
6405
6641
|
* would read every channel as its floor the moment one was bound.
|
|
@@ -6972,11 +7208,20 @@ export class WebGPURenderer {
|
|
|
6972
7208
|
* before last. That is the defined state the two-backends rule asks for.
|
|
6973
7209
|
*/
|
|
6974
7210
|
const refracting = this.bindSeeThrough(options);
|
|
7211
|
+
/* A dithered cutout tests hard on a blended draw, which has nothing to average a pattern with;
|
|
7212
|
+
put back below with the rest. See `cutoutDither.ts`. */
|
|
7213
|
+
const ditheredBlend = blend && this.cutoutResolveStaged !== 'hard';
|
|
7214
|
+
if (ditheredBlend)
|
|
7215
|
+
this.perFrameFloats[this.materialField('uCutout') + 1] = 0;
|
|
6975
7216
|
/* Taken for itself when any of those differs from the pass, and put back below: the rule both
|
|
6976
7217
|
backends count material changes by. See `materialChanges.ts`. */
|
|
6977
|
-
const own = ownsMaterial({ opacity, lit, fog, toneMapped, refracting }, this.surfaceFog);
|
|
7218
|
+
const own = ownsMaterial({ opacity, lit, fog, toneMapped, refracting }, this.surfaceFog) || ditheredBlend;
|
|
6978
7219
|
if (own)
|
|
6979
7220
|
this.materials.dirty();
|
|
7221
|
+
/* An eye's axis is the draw's, written into the open material. See `eyeAxisInWorld`. */
|
|
7222
|
+
const modelKind = this.materialModel?.kind ?? null;
|
|
7223
|
+
if (modelKind === 'eye')
|
|
7224
|
+
this.writeEyeAxis(model, this.skinPaletteSlot >= 0 && mesh.isSkinned);
|
|
6980
7225
|
const base = mesh.key ?? 'flat:s0:u0';
|
|
6981
7226
|
/*
|
|
6982
7227
|
* A rigged mesh takes the skinned vertex variant, an unrigged one the plain variant, and the
|
|
@@ -6988,7 +7233,12 @@ export class WebGPURenderer {
|
|
|
6988
7233
|
const skinned = this.skinPaletteSlot >= 0 && mesh.isSkinned;
|
|
6989
7234
|
const morphed = this.morphWeights !== null && mesh.morph !== null;
|
|
6990
7235
|
const deltas = morphed && mesh.morph !== null ? mesh.morph.view() : null;
|
|
6991
|
-
const
|
|
7236
|
+
const eight = skinned && mesh.isSkinnedEight;
|
|
7237
|
+
/* A cloth set and a skinned draw: the pipeline built with CLOTH_BOUND on, warmed when the
|
|
7238
|
+
binding was made. See `createClothBinding`. */
|
|
7239
|
+
const clothed = skinned && this.cloth !== null;
|
|
7240
|
+
const keyed = `${base}${morphed ? '|morph' : ''}${skinned ? (eight ? '|skin8' : '|skin') : ''}` +
|
|
7241
|
+
`${clothed ? '|cloth' : ''}`;
|
|
6992
7242
|
/*
|
|
6993
7243
|
* Depth writing and the overlay layer are pipeline state on this backend, so they belong in
|
|
6994
7244
|
* the key: two draws of one mesh differing only in these would otherwise share a cached
|
|
@@ -7003,13 +7253,22 @@ export class WebGPURenderer {
|
|
|
7003
7253
|
const layer = Math.min(Math.max(Math.round(options.depthLayer ?? 0), 0), MAX_DEPTH_LAYER);
|
|
7004
7254
|
/* Added rather than blended over: its own blend state, so its own pipeline. */
|
|
7005
7255
|
const adds = blend && options.additive === true;
|
|
7256
|
+
const coverage = !blend && this.cutoutResolveStaged === 'coverage';
|
|
7257
|
+
/* Skin under the screen-space blur draws its two halves; see `keepSkinDiffuse`. */
|
|
7258
|
+
const splits = modelKind === 'skin' && !blend && this.skinSplits();
|
|
7006
7259
|
const key = `${keyed}${blend ? (adds ? '|add' : '|blend') : ''}` +
|
|
7007
7260
|
`${depthWrite ? '' : '|nodepth'}${layer > 0 ? `|dl${layer}` : ''}` +
|
|
7008
7261
|
/* The two order-independent buffers are two formats and two blend states, so a draw into
|
|
7009
7262
|
one must never share a cached pipeline with the same mesh drawn into the other. */
|
|
7010
7263
|
`${this.oitMode === 'none' ? '' : `|oit${this.oitMode}`}` +
|
|
7011
7264
|
/* Culling is pipeline state here: a two-sided surface culls nothing. */
|
|
7012
|
-
`${this.materialDoubleSided ? '|2s' : ''}
|
|
7265
|
+
`${this.materialDoubleSided ? '|2s' : ''}` +
|
|
7266
|
+
/* And so is alpha-to-coverage, for a dithered cutout in a multisampled frame. */
|
|
7267
|
+
`${coverage ? '|a2c' : ''}` +
|
|
7268
|
+
/* And the material's shading model, which is the fragment stage's own switch. */
|
|
7269
|
+
`${modelKind === null ? '' : `|m:${modelKind}`}` +
|
|
7270
|
+
/* And which half of a skin it draws, under the screen-space blur. */
|
|
7271
|
+
`${splits ? '|ss' : ''}`;
|
|
7013
7272
|
/*
|
|
7014
7273
|
* The morph uniforms, written only for a morphed draw and read from the *morphed* variant's
|
|
7015
7274
|
* field table. `FLAT_VERT_FIELDS` is the plain variant's and does not name them at all — every
|
|
@@ -7039,7 +7298,7 @@ export class WebGPURenderer {
|
|
|
7039
7298
|
if (present === undefined) {
|
|
7040
7299
|
throw new Error(`WebGPU: no pipeline for ${key}; createMesh builds it`);
|
|
7041
7300
|
}
|
|
7042
|
-
pipeline = flatPipeline(pipelines, this.surface.device, this.flatLayoutFor(skinned, morphed), this.variant, key, present, adds ? 'additive' : blend, skinned, morphed, depthWrite, layer, false, this.oitMode, this.materialDoubleSided);
|
|
7301
|
+
pipeline = flatPipeline(pipelines, this.surface.device, this.flatLayoutFor(skinned, morphed), this.variant, key, present, adds ? 'additive' : blend, skinned, morphed, depthWrite, layer, false, this.oitMode, this.materialDoubleSided, coverage, eight, clothed, modelKind, splits ? 'scene' : 'whole');
|
|
7043
7302
|
}
|
|
7044
7303
|
/*
|
|
7045
7304
|
* Guards the draw call only, not the restore below.
|
|
@@ -7093,6 +7352,9 @@ export class WebGPURenderer {
|
|
|
7093
7352
|
pass.setIndexBuffer(mesh.indexBuffer, 'uint32');
|
|
7094
7353
|
pass.drawIndexed(mesh.indexCount);
|
|
7095
7354
|
}
|
|
7355
|
+
if (splits) {
|
|
7356
|
+
this.keepSkinDiffuse(mesh, key, base, skinned, morphed, deltas, layer, eight, clothed, slot, material);
|
|
7357
|
+
}
|
|
7096
7358
|
}
|
|
7097
7359
|
/* Back to the defaults for whatever is drawn next, exactly as `renderer.ts` restores them —
|
|
7098
7360
|
unconditionally, because the scratch state above is shared by every draw after this one
|
|
@@ -7113,6 +7375,10 @@ export class WebGPURenderer {
|
|
|
7113
7375
|
if (!toneMapped) {
|
|
7114
7376
|
this.perFrameInts[this.materialField('uOutputTransform')] = this.gradeCode();
|
|
7115
7377
|
}
|
|
7378
|
+
if (ditheredBlend) {
|
|
7379
|
+
this.perFrameFloats[this.materialField('uCutout') + 1] =
|
|
7380
|
+
CUTOUT_RESOLVE_CODE[this.cutoutResolveStaged];
|
|
7381
|
+
}
|
|
7116
7382
|
if (own)
|
|
7117
7383
|
this.materials.dirty();
|
|
7118
7384
|
}
|
|
@@ -7332,9 +7598,17 @@ export class WebGPURenderer {
|
|
|
7332
7598
|
const layer = Math.min(Math.max(Math.round(options.depthLayer ?? 0), 0), MAX_DEPTH_LAYER);
|
|
7333
7599
|
/* A blended batch shows what is behind it — refraction or glass — as a single draw does. */
|
|
7334
7600
|
const refracting = blend && this.bindSeeThrough(options);
|
|
7335
|
-
|
|
7601
|
+
/* As a single draw: a blended batch tests a dithered cutout hard, an opaque one in a
|
|
7602
|
+
multisampled frame takes coverage. See `cutoutDither.ts`. */
|
|
7603
|
+
const ditheredBlend = blend && this.cutoutResolveStaged !== 'hard';
|
|
7604
|
+
if (ditheredBlend)
|
|
7605
|
+
this.perFrameFloats[this.materialField('uCutout') + 1] = 0;
|
|
7606
|
+
const coverage = !blend && this.cutoutResolveStaged === 'coverage';
|
|
7607
|
+
const own = ownsMaterial({ opacity, lit, fog, toneMapped, refracting }, this.surfaceFog) || ditheredBlend;
|
|
7336
7608
|
if (own)
|
|
7337
7609
|
this.materials.dirty();
|
|
7610
|
+
/* A batch shades by its material's model too; an eye's axis is a placement's, not a batch's. */
|
|
7611
|
+
const modelKind = this.materialModel?.kind ?? null;
|
|
7338
7612
|
const base = mesh.key ?? 'flat:s0:u0';
|
|
7339
7613
|
/* `|inst` in the key, for the reason every other suffix is there: an instanced pipeline binds
|
|
7340
7614
|
a third vertex buffer and a different vertex module, and sharing a cache entry with the
|
|
@@ -7342,7 +7616,8 @@ export class WebGPURenderer {
|
|
|
7342
7616
|
const adds = blend && options.additive === true;
|
|
7343
7617
|
const key = `${base}|inst${blend ? (adds ? '|add' : '|blend') : ''}` +
|
|
7344
7618
|
`${depthWrite ? '' : '|nodepth'}${layer > 0 ? `|dl${layer}` : ''}` +
|
|
7345
|
-
`${this.materialDoubleSided ? '|2s' : ''}
|
|
7619
|
+
`${this.materialDoubleSided ? '|2s' : ''}${coverage ? '|a2c' : ''}` +
|
|
7620
|
+
`${modelKind === null ? '' : `|m:${modelKind}`}`;
|
|
7346
7621
|
const pipelines = blend ? this.blendedPipelines(lands) : this.targetPipelines();
|
|
7347
7622
|
let pipeline = pipelines.peek(key);
|
|
7348
7623
|
if (pipeline === undefined) {
|
|
@@ -7350,7 +7625,7 @@ export class WebGPURenderer {
|
|
|
7350
7625
|
if (present === undefined) {
|
|
7351
7626
|
throw new Error(`WebGPU: no mesh attributes recorded for ${base}; createMesh records them`);
|
|
7352
7627
|
}
|
|
7353
|
-
pipeline = flatPipeline(pipelines, this.surface.device, this.flatLayoutFor(false, false), this.variant, key, present, adds ? 'additive' : blend, false, false, depthWrite, layer, true, 'none', this.materialDoubleSided);
|
|
7628
|
+
pipeline = flatPipeline(pipelines, this.surface.device, this.flatLayoutFor(false, false), this.variant, key, present, adds ? 'additive' : blend, false, false, depthWrite, layer, true, 'none', this.materialDoubleSided, coverage, false, false, modelKind);
|
|
7354
7629
|
}
|
|
7355
7630
|
const material = this.materialSlotForDraw();
|
|
7356
7631
|
if (material !== null) {
|
|
@@ -7408,6 +7683,10 @@ export class WebGPURenderer {
|
|
|
7408
7683
|
}
|
|
7409
7684
|
if (refracting)
|
|
7410
7685
|
this.releaseSeeThrough();
|
|
7686
|
+
if (ditheredBlend) {
|
|
7687
|
+
this.perFrameFloats[this.materialField('uCutout') + 1] =
|
|
7688
|
+
CUTOUT_RESOLVE_CODE[this.cutoutResolveStaged];
|
|
7689
|
+
}
|
|
7411
7690
|
if (own)
|
|
7412
7691
|
this.materials.dirty();
|
|
7413
7692
|
}
|
|
@@ -7625,12 +7904,16 @@ export class WebGPURenderer {
|
|
|
7625
7904
|
this.bindSceneCasterMaterial(material);
|
|
7626
7905
|
this.drawMesh(mesh, model);
|
|
7627
7906
|
},
|
|
7628
|
-
skinnedMesh: (mesh, model, palette, material) => {
|
|
7907
|
+
skinnedMesh: (mesh, model, palette, material, cloth) => {
|
|
7629
7908
|
this.bindSceneCasterMaterial(material);
|
|
7630
7909
|
this.setSkinPalette(palette);
|
|
7910
|
+
if (cloth !== undefined) {
|
|
7911
|
+
this.setCloth(cloth.binding, cloth.particles);
|
|
7912
|
+
}
|
|
7631
7913
|
this.drawMesh(mesh, model);
|
|
7632
7914
|
/* Put back, or the next rigid draw in the replay skins by whatever this one left bound. */
|
|
7633
7915
|
this.setSkinPalette(null);
|
|
7916
|
+
this.setCloth(null);
|
|
7634
7917
|
},
|
|
7635
7918
|
instanced: (batch, data, material) => {
|
|
7636
7919
|
this.bindSceneCasterMaterial(material);
|
|
@@ -8380,14 +8663,20 @@ export class WebGPURenderer {
|
|
|
8380
8663
|
/* Slot `k` caches at `k + 1`; index 0 is the morphed draw that is not skinned and has no
|
|
8381
8664
|
slot of its own. See `skinnedGroups`. */
|
|
8382
8665
|
const at = skinned ? this.skinPaletteSlot + 1 : 0;
|
|
8666
|
+
/* A skinned group carries the cloth's views, or the stand-in's: see `clothViews`. */
|
|
8667
|
+
const cloth = skinned ? this.clothViews() : null;
|
|
8383
8668
|
if ((this.skinnedGroups[at] ?? null) === null ||
|
|
8384
8669
|
this.skinnedGroupsBeside[at] !== this.bindGroup ||
|
|
8385
8670
|
this.skinnedGroupsPalette[at] !== palette ||
|
|
8386
|
-
this.skinnedGroupsDeltas[at] !== deltas
|
|
8387
|
-
this.
|
|
8671
|
+
this.skinnedGroupsDeltas[at] !== deltas ||
|
|
8672
|
+
this.skinnedGroupsCloth[at] !== (cloth?.particles ?? null) ||
|
|
8673
|
+
this.skinnedGroupsClothBinding[at] !== (cloth?.binding ?? null)) {
|
|
8674
|
+
this.skinnedGroups[at] = createFlatBindGroup(this.surface.device, this.flatLayoutFor(skinned, morphed), this.variant, this.perDraw.buffer, this.perFrame.buffer, this.flatTextures, palette, deltas, cloth);
|
|
8388
8675
|
this.skinnedGroupsBeside[at] = this.bindGroup;
|
|
8389
8676
|
this.skinnedGroupsPalette[at] = palette;
|
|
8390
8677
|
this.skinnedGroupsDeltas[at] = deltas;
|
|
8678
|
+
this.skinnedGroupsCloth[at] = cloth?.particles ?? null;
|
|
8679
|
+
this.skinnedGroupsClothBinding[at] = cloth?.binding ?? null;
|
|
8391
8680
|
}
|
|
8392
8681
|
return this.skinnedGroups[at];
|
|
8393
8682
|
}
|
|
@@ -8456,16 +8745,19 @@ export class WebGPURenderer {
|
|
|
8456
8745
|
const normalMap = this.normalMap;
|
|
8457
8746
|
const ormMap = this.ormMap;
|
|
8458
8747
|
const emissiveMap = this.emissiveMap;
|
|
8748
|
+
const modelMap = this.modelMap;
|
|
8459
8749
|
this.albedo = null;
|
|
8460
8750
|
this.normalMap = null;
|
|
8461
8751
|
this.ormMap = null;
|
|
8462
8752
|
this.emissiveMap = null;
|
|
8753
|
+
this.modelMap = null;
|
|
8463
8754
|
this.blankAlbedoBindGroup = this.buildFlatBindGroup();
|
|
8464
8755
|
this.albedo = albedo;
|
|
8465
8756
|
this.normalMap = normalMap;
|
|
8466
8757
|
this.ormMap = ormMap;
|
|
8467
8758
|
this.emissiveMap = emissiveMap;
|
|
8468
|
-
this.
|
|
8759
|
+
this.modelMap = modelMap;
|
|
8760
|
+
this.bindGroup = this.flatGroupForMaps(this.albedo, this.normalMap, this.ormMap, this.emissiveMap, this.modelMap);
|
|
8469
8761
|
/* The open material lives in a slot of a buffer that no longer exists. */
|
|
8470
8762
|
this.materials.dirty();
|
|
8471
8763
|
}
|
|
@@ -8503,13 +8795,14 @@ export class WebGPURenderer {
|
|
|
8503
8795
|
if (this.albedo === null &&
|
|
8504
8796
|
this.normalMap === null &&
|
|
8505
8797
|
this.ormMap === null &&
|
|
8506
|
-
this.emissiveMap === null
|
|
8798
|
+
this.emissiveMap === null &&
|
|
8799
|
+
this.modelMap === null) {
|
|
8507
8800
|
this.blankAlbedoBindGroup = this.buildFlatBindGroup();
|
|
8508
8801
|
this.bindGroupBudget.ask();
|
|
8509
8802
|
this.bindGroup = this.blankAlbedoBindGroup;
|
|
8510
8803
|
return;
|
|
8511
8804
|
}
|
|
8512
|
-
this.bindGroup = this.flatGroupForMaps(this.albedo, this.normalMap, this.ormMap, this.emissiveMap);
|
|
8805
|
+
this.bindGroup = this.flatGroupForMaps(this.albedo, this.normalMap, this.ormMap, this.emissiveMap, this.modelMap);
|
|
8513
8806
|
}
|
|
8514
8807
|
/**
|
|
8515
8808
|
* The sky, as one triangle covering the screen with the world reconstructed per fragment.
|
|
@@ -9168,6 +9461,9 @@ export class WebGPURenderer {
|
|
|
9168
9461
|
* the only moment they can land; everything else keeps the path it has.
|
|
9169
9462
|
*/
|
|
9170
9463
|
recordBlended(reads, lands = this.drawsLate()) {
|
|
9464
|
+
/* Skin is spread and added before anything blended lands over it. See `runSkinScatter`. */
|
|
9465
|
+
if (this.skinScatter !== null && !this.skinScattered)
|
|
9466
|
+
this.runSkinScatter();
|
|
9171
9467
|
const late = this.latePass;
|
|
9172
9468
|
if (late !== null && lands)
|
|
9173
9469
|
return late.take();
|
|
@@ -9233,6 +9529,14 @@ export class WebGPURenderer {
|
|
|
9233
9529
|
const dynamic = mesh.motion;
|
|
9234
9530
|
const rows = dynamic?.previous ?? null;
|
|
9235
9531
|
const rewritten = rows !== null && dynamic !== null && dynamic.changed === this.moverFrame;
|
|
9532
|
+
const cloth = this.cloth;
|
|
9533
|
+
if (cloth !== null &&
|
|
9534
|
+
this.skinPaletteSlot >= 0 &&
|
|
9535
|
+
mesh.isSkinned &&
|
|
9536
|
+
mesh.skinOffsets !== null) {
|
|
9537
|
+
this.recordClothMotion(mesh, model, previous, cloth);
|
|
9538
|
+
return;
|
|
9539
|
+
}
|
|
9236
9540
|
if (previous === null) {
|
|
9237
9541
|
if (rewritten)
|
|
9238
9542
|
this.motionPass?.recordDynamic(mesh, model, model, rows);
|
|
@@ -9277,6 +9581,41 @@ export class WebGPURenderer {
|
|
|
9277
9581
|
return;
|
|
9278
9582
|
this.motionPass?.recordSkinned(mesh, model, mover.previousModel, now, was);
|
|
9279
9583
|
}
|
|
9584
|
+
/**
|
|
9585
|
+
* A cloth-bound draw's motion. **Recorded even where the caller named no previous place**, as a
|
|
9586
|
+
* rewritten mesh's is: the particles carry their own past, and a garment swinging on a character
|
|
9587
|
+
* standing still is motion nobody else can see. Where the rig is a `Mover` its last pose is taken
|
|
9588
|
+
* as a skinned mover's is; otherwise the rig is taken not to have moved.
|
|
9589
|
+
*/
|
|
9590
|
+
recordClothMotion(mesh, model, previous, cloth) {
|
|
9591
|
+
const now = this.skinPalettes.view(this.skinPaletteSlot);
|
|
9592
|
+
if (now === null)
|
|
9593
|
+
return;
|
|
9594
|
+
let was = now;
|
|
9595
|
+
let wasModel = model;
|
|
9596
|
+
if (previous !== null && isMover(previous)) {
|
|
9597
|
+
const verdict = advanceMover(previous, this.moverRenderer, this.moverFrame, model, this.skinPaletteData);
|
|
9598
|
+
if (verdict === MOVER_TWICE) {
|
|
9599
|
+
this.warnMoverTwice();
|
|
9600
|
+
return;
|
|
9601
|
+
}
|
|
9602
|
+
if (verdict === MOVER_MOTION) {
|
|
9603
|
+
const slot = this.skinPalettes.take(this.surface.device, previous.previousPalette);
|
|
9604
|
+
was = slot === null ? null : this.skinPalettes.view(slot);
|
|
9605
|
+
wasModel = previous.previousModel;
|
|
9606
|
+
}
|
|
9607
|
+
}
|
|
9608
|
+
else if (previous !== null) {
|
|
9609
|
+
wasModel = previous;
|
|
9610
|
+
}
|
|
9611
|
+
if (was === null)
|
|
9612
|
+
return;
|
|
9613
|
+
const particlesWas = cloth.particles.previousAt(this.moverFrame);
|
|
9614
|
+
/* Nothing said it moved and nothing did: the camera's motion, derived from depth, is the answer. */
|
|
9615
|
+
if (previous === null && particlesWas === cloth.particles.current)
|
|
9616
|
+
return;
|
|
9617
|
+
this.motionPass?.recordCloth(mesh, model, wasModel, now, was, cloth.binding.bindingView, cloth.particles.current, particlesWas);
|
|
9618
|
+
}
|
|
9280
9619
|
warnMoverTwice() {
|
|
9281
9620
|
if (this.warnedMoverTwice)
|
|
9282
9621
|
return;
|
|
@@ -10199,6 +10538,106 @@ export class WebGPURenderer {
|
|
|
10199
10538
|
this.refractSnapshotTaken = true;
|
|
10200
10539
|
return true;
|
|
10201
10540
|
}
|
|
10541
|
+
/**
|
|
10542
|
+
* Whether a skin draw now draws its two halves: the profile asked, the frame has not spread its
|
|
10543
|
+
* skin yet, and this is the frame's own picture — not a mirror's, a probe's, an overlay after
|
|
10544
|
+
* the frame, or an order-independent set, none of which the spread reads.
|
|
10545
|
+
*/
|
|
10546
|
+
skinSplits() {
|
|
10547
|
+
return (this.skinScatter !== null &&
|
|
10548
|
+
!this.skinScattered &&
|
|
10549
|
+
!this.reflectionPassActive &&
|
|
10550
|
+
!this.probePassActive &&
|
|
10551
|
+
!this.framePresented &&
|
|
10552
|
+
!this.overlayActive &&
|
|
10553
|
+
this.oitMode === 'none');
|
|
10554
|
+
}
|
|
10555
|
+
/**
|
|
10556
|
+
* A skin draw's diffuse half, kept for the spread: the frame's draw again, with the same slots,
|
|
10557
|
+
* buffers and bind group, through the pipeline that writes the diffuse alone into the blur's
|
|
10558
|
+
* target and tests for the depth its frame half wrote.
|
|
10559
|
+
*/
|
|
10560
|
+
keepSkinDiffuse(mesh, frameKey, base, skinned, morphed, deltas, layer, eight, clothed, slot, material) {
|
|
10561
|
+
const skin = this.skinScatter;
|
|
10562
|
+
const pipelines = this.skinPipelines;
|
|
10563
|
+
const present = this.meshPresent.get(base);
|
|
10564
|
+
if (skin === null || pipelines === null || present === undefined)
|
|
10565
|
+
return;
|
|
10566
|
+
const key = `${frameKey}|sd`;
|
|
10567
|
+
const pipeline = pipelines.peek(key) ??
|
|
10568
|
+
flatPipeline(pipelines, this.surface.device, this.flatLayoutFor(skinned, morphed), this.variant, key, present, false, skinned, morphed, true, layer, false, 'none', this.materialDoubleSided, false, eight, clothed, 'skin', 'diffuse');
|
|
10569
|
+
const command = skin.take();
|
|
10570
|
+
command.pipeline = pipeline;
|
|
10571
|
+
command.bindGroup = this.flatBindGroupFor(skinned, morphed, deltas);
|
|
10572
|
+
command.offsetA = slot;
|
|
10573
|
+
command.offsetB = material;
|
|
10574
|
+
command.offsetCount = 2;
|
|
10575
|
+
for (let index = 0; index < mesh.vertexBuffers.length; index++) {
|
|
10576
|
+
command.vertexBuffers[index] = mesh.vertexBuffers[index];
|
|
10577
|
+
}
|
|
10578
|
+
command.vertexCount = mesh.vertexBuffers.length;
|
|
10579
|
+
command.indexBuffer = mesh.indexBuffer;
|
|
10580
|
+
command.indexed = true;
|
|
10581
|
+
command.count = mesh.indexCount;
|
|
10582
|
+
command.instances = 1;
|
|
10583
|
+
command.indirect = null;
|
|
10584
|
+
}
|
|
10585
|
+
/**
|
|
10586
|
+
* Spread the frame's skin and add it back: once, at the first blended draw or before the frame's
|
|
10587
|
+
* last flush. **A fifth instance of the boundary `takeRefractSnapshot` describes** — flush, end
|
|
10588
|
+
* the pass, work between passes, reopen with `loadOp: 'load'` — and refused where that one is,
|
|
10589
|
+
* for its reasons. Between the passes: the kept diffuse halves into their target against the
|
|
10590
|
+
* frame's depth, the depth resolved, and `SkinScatterPass.spread` across and then down into the
|
|
10591
|
+
* frame's colour.
|
|
10592
|
+
*/
|
|
10593
|
+
runSkinScatter() {
|
|
10594
|
+
const skin = this.skinScatter;
|
|
10595
|
+
if (skin === null || this.skinScattered)
|
|
10596
|
+
return;
|
|
10597
|
+
/* Latched whether or not skin was kept: a skin drawn after this point is the whole surface. */
|
|
10598
|
+
this.skinScattered = true;
|
|
10599
|
+
if (!skin.pending)
|
|
10600
|
+
return;
|
|
10601
|
+
if (this.reflectionPassActive || this.probePassActive || this.oitReplaying)
|
|
10602
|
+
return;
|
|
10603
|
+
const encoder = this.encoder;
|
|
10604
|
+
const depth = this.depthView;
|
|
10605
|
+
const resolved = this.resolvedDepthView;
|
|
10606
|
+
const target = this.compositeTarget();
|
|
10607
|
+
const projection = this.frameProjection;
|
|
10608
|
+
if (encoder === null || depth === null || resolved === null)
|
|
10609
|
+
return;
|
|
10610
|
+
if (target === null || projection === null)
|
|
10611
|
+
return;
|
|
10612
|
+
if (this.pass === null && this.openPass() === null)
|
|
10613
|
+
return;
|
|
10614
|
+
if (this.pass === null)
|
|
10615
|
+
return;
|
|
10616
|
+
if (this.quality.frameGraph)
|
|
10617
|
+
this.flushGraph();
|
|
10618
|
+
this.pass.end();
|
|
10619
|
+
const size = this.sceneSize();
|
|
10620
|
+
skin.size(size.width, size.height, this.samples);
|
|
10621
|
+
skin.drawDiffuse(encoder, depth, this.issueBound, this.gpuTimer.writesFor());
|
|
10622
|
+
this.resolveDepth(encoder);
|
|
10623
|
+
mat4.invert(this.skinInverseProjection, projection);
|
|
10624
|
+
const multisampled = this.compositeMsaa();
|
|
10625
|
+
skin.spread(encoder, this.pipelines, resolved, multisampled ?? target, multisampled === null ? null : target, this.samples, projection, this.skinInverseProjection, () => this.gpuTimer.writesFor());
|
|
10626
|
+
this.pass = encoder.beginRenderPass({
|
|
10627
|
+
label: 'skin.scattered',
|
|
10628
|
+
timestampWrites: this.gpuTimer.writesFor(),
|
|
10629
|
+
colorAttachments: [
|
|
10630
|
+
{
|
|
10631
|
+
view: multisampled ?? target,
|
|
10632
|
+
resolveTarget: multisampled !== null ? target : undefined,
|
|
10633
|
+
/* Loaded, not cleared: the frame so far, with its skin added. */
|
|
10634
|
+
loadOp: 'load',
|
|
10635
|
+
storeOp: resolvedStoreOp(multisampled !== null, false, this.quality.discardResolvedAttachments),
|
|
10636
|
+
},
|
|
10637
|
+
],
|
|
10638
|
+
depthStencilAttachment: { view: depth, depthLoadOp: 'load', depthStoreOp: 'store' },
|
|
10639
|
+
});
|
|
10640
|
+
}
|
|
10202
10641
|
takeVolumeDepth() {
|
|
10203
10642
|
/*
|
|
10204
10643
|
* **Never while the mirror is open, and this is a correctness fix rather than a saving.**
|
|
@@ -10821,6 +11260,8 @@ export class WebGPURenderer {
|
|
|
10821
11260
|
* `beginFrame` resets it. Silent, and exactly the kind of loss the arena is designed
|
|
10822
11261
|
* against everywhere else.
|
|
10823
11262
|
*/
|
|
11263
|
+
/* Before the last flush, which may let the depth go that the spread still reads. */
|
|
11264
|
+
this.runSkinScatter();
|
|
10824
11265
|
this.flushGraph('boundary', this.liveOutFinalFlush());
|
|
10825
11266
|
/*
|
|
10826
11267
|
* `ensurePass` rather than a null check: the frame's pass is opened on demand now, so a
|
|
@@ -13638,6 +14079,7 @@ export class WebGPURenderer {
|
|
|
13638
14079
|
* the 2026-08-13 rule asks for — one decision, bound twice.
|
|
13639
14080
|
*/
|
|
13640
14081
|
dispose() {
|
|
14082
|
+
this.skinScatter?.dispose();
|
|
13641
14083
|
this.motionPass?.dispose();
|
|
13642
14084
|
this.motionPass = null;
|
|
13643
14085
|
this.latePass?.dispose();
|
|
@@ -13677,3 +14119,11 @@ export class WebGPURenderer {
|
|
|
13677
14119
|
this.surface.dispose();
|
|
13678
14120
|
}
|
|
13679
14121
|
}
|
|
14122
|
+
/**
|
|
14123
|
+
* The views a binding and a character's particles bind as, built once a pair and kept on the
|
|
14124
|
+
* particles: a bind group is cached by its views' identity, so a fresh object a draw would rebuild
|
|
14125
|
+
* the group every draw.
|
|
14126
|
+
*/
|
|
14127
|
+
function clothViewsOf(binding, particles) {
|
|
14128
|
+
return particles.viewsWith(binding);
|
|
14129
|
+
}
|