@driftengine/core 4.8.3 → 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 +71 -0
- package/README.md +1 -1
- 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/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
|
@@ -164,6 +164,15 @@ import {
|
|
|
164
164
|
import type { TextStyle } from '../../textLayout.ts';
|
|
165
165
|
import { deviceSnappedCellSize, deviceSnappedOrigin } from '../../textLayout.ts';
|
|
166
166
|
import { GpuSurfaceTexture } from './surfaceTexturePass.ts';
|
|
167
|
+
import { gpuCompressedFormats } from './compressedUpload.ts';
|
|
168
|
+
import { GpuClothBinding, GpuClothParticles, clothStandIn } from './clothTextures.ts';
|
|
169
|
+
import type { ClothViews } from './clothTextures.ts';
|
|
170
|
+
import type { ComputeHost } from './computeHost.ts';
|
|
171
|
+
import { MODEL_PARAM_FLOATS, eyeAxisInWorld, packModel } from '../../surfaceModel.ts';
|
|
172
|
+
import type { SurfaceModel } from '../../surfaceModel.ts';
|
|
173
|
+
import { validateClothBinding } from '../../clothBindingData.ts';
|
|
174
|
+
import type { ClothBindingData } from '../../clothBindingData.ts';
|
|
175
|
+
import type { CompressedTextureFormat, SurfaceSource } from '../../compressedSource.ts';
|
|
167
176
|
import {
|
|
168
177
|
createSdfTextBindGroup,
|
|
169
178
|
createSdfTextBindGroupLayout,
|
|
@@ -383,6 +392,7 @@ import {
|
|
|
383
392
|
depthPipeline,
|
|
384
393
|
} from './depthPass.ts';
|
|
385
394
|
import { PipelineCache, type LitSwitch } from './pipelineCache.ts';
|
|
395
|
+
import { SKIN_TARGET_FORMAT, SkinScatterPass } from './skinScatterPass.ts';
|
|
386
396
|
import {
|
|
387
397
|
SKY_FIELDS,
|
|
388
398
|
SKY_UNIFORM_SIZE,
|
|
@@ -583,6 +593,8 @@ import type { Arena, FlushSchedule, ScheduledPass } from '../../frame/index.ts';
|
|
|
583
593
|
import { GpuInstancedBatch } from './instanced.ts';
|
|
584
594
|
import type { InstancedOptions, MeshInstances } from '../../instances.ts';
|
|
585
595
|
import { cutoutOf } from '../../cutoutCaster.ts';
|
|
596
|
+
import { CUTOUT_RESOLVE_CODE, resolveCutout } from '../../cutoutDither.ts';
|
|
597
|
+
import type { CutoutResolve } from '../../cutoutDither.ts';
|
|
586
598
|
import type { CutoutCaster } from '../../cutoutCaster.ts';
|
|
587
599
|
import { LightField } from '../../driftLight/lightField.ts';
|
|
588
600
|
import type { LightFieldOptions, LightFieldSource } from '../../driftLight/lightField.ts';
|
|
@@ -1457,6 +1469,12 @@ export class WebGPURenderer implements RendererApi {
|
|
|
1457
1469
|
private readonly skinnedGroupsBeside: (GPUBindGroup | null)[] = [];
|
|
1458
1470
|
private readonly skinnedGroupsPalette: (GPUTextureView | null)[] = [];
|
|
1459
1471
|
private readonly skinnedGroupsDeltas: (GPUTextureView | null)[] = [];
|
|
1472
|
+
/** The cloth views each skinned twin was built with, compared by identity like the palette. */
|
|
1473
|
+
private readonly skinnedGroupsCloth: (GPUTextureView | null)[] = [];
|
|
1474
|
+
private readonly skinnedGroupsClothBinding: (GPUTextureView | null)[] = [];
|
|
1475
|
+
/** The cloth the next skinned draws are placed by, or null. See `setCloth`. */
|
|
1476
|
+
private cloth: { binding: GpuClothBinding; particles: GpuClothParticles } | null = null;
|
|
1477
|
+
private clothStandCache: { texture: GPUTexture; views: ClothViews } | null = null;
|
|
1460
1478
|
/**
|
|
1461
1479
|
* Which palette slot the following draws skin by, or -1 for none.
|
|
1462
1480
|
*
|
|
@@ -1872,6 +1890,16 @@ export class WebGPURenderer implements RendererApi {
|
|
|
1872
1890
|
private refractSnapshotView: GPUTextureView | null = null;
|
|
1873
1891
|
/** Whether this frame has taken it. One snapshot serves every refracting draw. */
|
|
1874
1892
|
private refractSnapshotTaken = false;
|
|
1893
|
+
/**
|
|
1894
|
+
* Skin's screen-space scattering, made where the profile asks for it and has a composite to add
|
|
1895
|
+
* into, and null otherwise — which is every profile by default. See `SkinScatterPass`.
|
|
1896
|
+
*/
|
|
1897
|
+
private readonly skinScatter: SkinScatterPass | null;
|
|
1898
|
+
/** The pipelines skin's diffuse half is built for: the blur target's format, the frame's samples. */
|
|
1899
|
+
private readonly skinPipelines: PipelineCache | null;
|
|
1900
|
+
/** Whether this frame has spread its skin: once, at its first blended draw or before its end. */
|
|
1901
|
+
private skinScattered = false;
|
|
1902
|
+
private readonly skinInverseProjection = mat4.create();
|
|
1875
1903
|
private sceneColorView: GPUTextureView | null = null;
|
|
1876
1904
|
/** The multisampled twin, when the profile asks for samples. Resolved into `sceneColor`. */
|
|
1877
1905
|
private sceneColorMsaa: GPUTexture | null = null;
|
|
@@ -2052,6 +2080,17 @@ export class WebGPURenderer implements RendererApi {
|
|
|
2052
2080
|
private temporalJitterX = 0;
|
|
2053
2081
|
private temporalJitterY = 0;
|
|
2054
2082
|
private temporalJittering = false;
|
|
2083
|
+
/**
|
|
2084
|
+
* The frame a dithered cutout's pattern is offset by, advanced at `beginFrame` and wrapped at 64.
|
|
2085
|
+
* See `cutoutDither.ts`.
|
|
2086
|
+
*/
|
|
2087
|
+
private cutoutFrame = 0;
|
|
2088
|
+
/**
|
|
2089
|
+
* How the current material's cutout edge is drawn this frame, decided when the material is staged
|
|
2090
|
+
* — after `bindMeshPass` has settled whether the frame is temporal — and read by every draw: a
|
|
2091
|
+
* coverage edge is pipeline state here (`|a2c`), and a blended draw tests hard.
|
|
2092
|
+
*/
|
|
2093
|
+
private cutoutResolveStaged: CutoutResolve = 'hard';
|
|
2055
2094
|
/**
|
|
2056
2095
|
* Whether this frame is being reconstructed, settled once in `bindMeshPass`.
|
|
2057
2096
|
*
|
|
@@ -2769,6 +2808,12 @@ export class WebGPURenderer implements RendererApi {
|
|
|
2769
2808
|
private ormMap: GpuSurfaceTexture | null = null;
|
|
2770
2809
|
/** The caller's emissive map, or null for the stand-in. See `setMaterial`. */
|
|
2771
2810
|
private emissiveMap: GpuSurfaceTexture | null = null;
|
|
2811
|
+
/** The material's model map, or null for the stand-in. See `setMaterial` and `surfaceModel.ts`. */
|
|
2812
|
+
private modelMap: GpuSurfaceTexture | null = null;
|
|
2813
|
+
/** The material's shading model, which picks the lit pipeline: null the standard. */
|
|
2814
|
+
private materialModel: SurfaceModel | null = null;
|
|
2815
|
+
/** Its numbers, `uModelParams`, packed by `packModel`. */
|
|
2816
|
+
private readonly modelParams = new Float32Array(MODEL_PARAM_FLOATS);
|
|
2772
2817
|
/**
|
|
2773
2818
|
* The flat group for a material, keyed on **all four** of its maps.
|
|
2774
2819
|
*
|
|
@@ -2799,7 +2844,10 @@ export class WebGPURenderer implements RendererApi {
|
|
|
2799
2844
|
GpuSurfaceTexture | null,
|
|
2800
2845
|
Map<
|
|
2801
2846
|
GpuSurfaceTexture | null,
|
|
2802
|
-
Map<
|
|
2847
|
+
Map<
|
|
2848
|
+
GpuSurfaceTexture | null,
|
|
2849
|
+
Map<GpuSurfaceTexture | null, Map<GpuSurfaceTexture | null, GPUBindGroup>>
|
|
2850
|
+
>
|
|
2803
2851
|
>
|
|
2804
2852
|
>();
|
|
2805
2853
|
/** The group holding the one-pixel stand-in, for every draw with no texture set. */
|
|
@@ -2819,18 +2867,42 @@ export class WebGPURenderer implements RendererApi {
|
|
|
2819
2867
|
private materialHasMaps = false;
|
|
2820
2868
|
|
|
2821
2869
|
createSurfaceTexture(
|
|
2822
|
-
source:
|
|
2870
|
+
source: SurfaceSource,
|
|
2823
2871
|
options: SurfaceTextureOptions = {},
|
|
2824
2872
|
): GpuSurfaceTexture {
|
|
2825
|
-
return new GpuSurfaceTexture(
|
|
2873
|
+
return new GpuSurfaceTexture(
|
|
2874
|
+
this.surface.device,
|
|
2875
|
+
this.pipelines,
|
|
2876
|
+
source,
|
|
2877
|
+
options,
|
|
2878
|
+
this.compressedFormats,
|
|
2879
|
+
);
|
|
2880
|
+
}
|
|
2881
|
+
|
|
2882
|
+
private compressedCache: readonly CompressedTextureFormat[] | null = null;
|
|
2883
|
+
|
|
2884
|
+
/**
|
|
2885
|
+
* The BC formats this device takes as blocks: every one where the device carries
|
|
2886
|
+
* `texture-compression-bc`, which `select.ts` asks for wherever the adapter offers it, and none
|
|
2887
|
+
* where it does not. See `Renderer.compressedFormats`.
|
|
2888
|
+
*/
|
|
2889
|
+
get compressedFormats(): readonly CompressedTextureFormat[] {
|
|
2890
|
+
this.compressedCache ??= gpuCompressedFormats(this.surface.device);
|
|
2891
|
+
return this.compressedCache;
|
|
2826
2892
|
}
|
|
2827
2893
|
|
|
2828
2894
|
/** Several images of one size as an array's layers. See `RendererApi.createSurfaceTextureArray`. */
|
|
2829
2895
|
createSurfaceTextureArray(
|
|
2830
|
-
sources: readonly
|
|
2896
|
+
sources: readonly SurfaceSource[],
|
|
2831
2897
|
options: SurfaceTextureOptions = {},
|
|
2832
2898
|
): GpuSurfaceTexture {
|
|
2833
|
-
return new GpuSurfaceTexture(
|
|
2899
|
+
return new GpuSurfaceTexture(
|
|
2900
|
+
this.surface.device,
|
|
2901
|
+
this.pipelines,
|
|
2902
|
+
sources,
|
|
2903
|
+
options,
|
|
2904
|
+
this.compressedFormats,
|
|
2905
|
+
);
|
|
2834
2906
|
}
|
|
2835
2907
|
|
|
2836
2908
|
/**
|
|
@@ -2878,6 +2950,7 @@ export class WebGPURenderer implements RendererApi {
|
|
|
2878
2950
|
if (this.normalMap === texture) this.normalMap = null;
|
|
2879
2951
|
if (this.ormMap === texture) this.ormMap = null;
|
|
2880
2952
|
if (this.emissiveMap === texture) this.emissiveMap = null;
|
|
2953
|
+
if (this.modelMap === texture) this.modelMap = null;
|
|
2881
2954
|
this.forgetBindingsOf(texture);
|
|
2882
2955
|
texture.dispose();
|
|
2883
2956
|
}
|
|
@@ -2903,7 +2976,13 @@ export class WebGPURenderer implements RendererApi {
|
|
|
2903
2976
|
this.albedo = null;
|
|
2904
2977
|
this.blankAlbedoBindGroup = this.buildFlatBindGroup();
|
|
2905
2978
|
this.albedo = held;
|
|
2906
|
-
this.bindGroup = this.flatGroupForMaps(
|
|
2979
|
+
this.bindGroup = this.flatGroupForMaps(
|
|
2980
|
+
held,
|
|
2981
|
+
this.normalMap,
|
|
2982
|
+
this.ormMap,
|
|
2983
|
+
this.emissiveMap,
|
|
2984
|
+
this.modelMap,
|
|
2985
|
+
);
|
|
2907
2986
|
}
|
|
2908
2987
|
|
|
2909
2988
|
/**
|
|
@@ -2932,6 +3011,128 @@ export class WebGPURenderer implements RendererApi {
|
|
|
2932
3011
|
this.skinPaletteSlot = this.skinPalettes.take(this.surface.device, palette) ?? -1;
|
|
2933
3012
|
}
|
|
2934
3013
|
|
|
3014
|
+
/**
|
|
3015
|
+
* A mesh's cloth binding. See `RendererApi.createClothBinding`. The pipelines that draw it bound
|
|
3016
|
+
* are warmed here, every way this mesh can be drawn, so none compiles inside the first frame
|
|
3017
|
+
* that draws a garment.
|
|
3018
|
+
*/
|
|
3019
|
+
createClothBinding(mesh: GpuMesh, data: ClothBindingData): GpuClothBinding {
|
|
3020
|
+
if (!mesh.isSkinned) {
|
|
3021
|
+
throw new Error(
|
|
3022
|
+
'createClothBinding: this mesh has no rig, and a binding blends each vertex between its ' +
|
|
3023
|
+
'skinning and the cloth',
|
|
3024
|
+
);
|
|
3025
|
+
}
|
|
3026
|
+
validateClothBinding(data, mesh.vertexCount);
|
|
3027
|
+
const base = (mesh as GpuMesh & { key?: string }).key ?? 'flat:s0:u0';
|
|
3028
|
+
const present = this.meshPresent.get(base);
|
|
3029
|
+
if (present !== undefined) {
|
|
3030
|
+
const skin = mesh.isSkinnedEight ? '|skin8' : '|skin';
|
|
3031
|
+
for (const morphed of mesh.morph !== null ? [false, true] : [false]) {
|
|
3032
|
+
for (const translucent of [false, true]) {
|
|
3033
|
+
void flatPipelineAsync(
|
|
3034
|
+
this.targetPipelines(),
|
|
3035
|
+
this.surface.device,
|
|
3036
|
+
this.flatLayoutFor(true, morphed),
|
|
3037
|
+
this.variant,
|
|
3038
|
+
`${base}${morphed ? '|morph' : ''}${skin}|cloth${translucent ? '|blend' : ''}`,
|
|
3039
|
+
present,
|
|
3040
|
+
translucent,
|
|
3041
|
+
true,
|
|
3042
|
+
morphed,
|
|
3043
|
+
mesh.isSkinnedEight,
|
|
3044
|
+
true,
|
|
3045
|
+
);
|
|
3046
|
+
}
|
|
3047
|
+
}
|
|
3048
|
+
}
|
|
3049
|
+
return new GpuClothBinding(this.surface.device, data);
|
|
3050
|
+
}
|
|
3051
|
+
|
|
3052
|
+
/** One character's particles. See `RendererApi.createClothParticles`. */
|
|
3053
|
+
createClothParticles(count: number): GpuClothParticles {
|
|
3054
|
+
return new GpuClothParticles(this.surface.device, count);
|
|
3055
|
+
}
|
|
3056
|
+
|
|
3057
|
+
/**
|
|
3058
|
+
* This frame's particles. **Once a frame, before the character's draws**: a texture write lands
|
|
3059
|
+
* ahead of every draw of the frame, so a second update between two draws would reach both.
|
|
3060
|
+
*/
|
|
3061
|
+
updateClothParticles(particles: GpuClothParticles, positions: Float32Array): void {
|
|
3062
|
+
if (this.surface.lost) return;
|
|
3063
|
+
/* A write between frames is the next frame's, as a rewritten mesh's is. See `changeFrames.ts`. */
|
|
3064
|
+
particles.update(this.surface.device, positions, frameOfChange(this.moverFrame, this.inFrame));
|
|
3065
|
+
}
|
|
3066
|
+
|
|
3067
|
+
/** Place the following skinned draws by a cloth, or by none. See `RendererApi.setCloth`. */
|
|
3068
|
+
setCloth(binding: GpuClothBinding | null, particles: GpuClothParticles | null = null): void {
|
|
3069
|
+
if (binding === null || particles === null) {
|
|
3070
|
+
this.cloth = null;
|
|
3071
|
+
return;
|
|
3072
|
+
}
|
|
3073
|
+
if (binding.particles !== particles.count) {
|
|
3074
|
+
throw new Error(
|
|
3075
|
+
`setCloth: the binding names ${binding.particles} particles and these are ${particles.count}`,
|
|
3076
|
+
);
|
|
3077
|
+
}
|
|
3078
|
+
this.cloth = { binding, particles };
|
|
3079
|
+
}
|
|
3080
|
+
|
|
3081
|
+
/** Retired rather than destroyed: a draw recorded earlier in this frame may still read them. */
|
|
3082
|
+
disposeClothBinding(binding: GpuClothBinding): void {
|
|
3083
|
+
this.retiredTextures.push(...binding.textures);
|
|
3084
|
+
}
|
|
3085
|
+
|
|
3086
|
+
disposeClothParticles(particles: GpuClothParticles): void {
|
|
3087
|
+
this.retiredTextures.push(...particles.textures);
|
|
3088
|
+
}
|
|
3089
|
+
|
|
3090
|
+
/**
|
|
3091
|
+
* The device and the frame's bookkeeping, for compute a consumer opts into by importing it —
|
|
3092
|
+
* `createSkinnedCloth` — rather than for anything this renderer runs itself. See `computeHost.ts`.
|
|
3093
|
+
*/
|
|
3094
|
+
computeHost(): ComputeHost {
|
|
3095
|
+
return this.computeHostCache;
|
|
3096
|
+
}
|
|
3097
|
+
|
|
3098
|
+
private readonly computeHostCache: ComputeHost = {
|
|
3099
|
+
device: () => this.surface.device,
|
|
3100
|
+
lost: () => this.surface.lost,
|
|
3101
|
+
changeFrame: () => frameOfChange(this.moverFrame, this.inFrame),
|
|
3102
|
+
retire: (textures) => {
|
|
3103
|
+
this.retiredTextures.push(...textures);
|
|
3104
|
+
},
|
|
3105
|
+
};
|
|
3106
|
+
|
|
3107
|
+
/**
|
|
3108
|
+
* An eye's axis in the world for this draw, into the open material's \`uModelParams\`, and a new
|
|
3109
|
+
* material slot when it moved: the axis is per draw, and a slot already taken by an earlier draw
|
|
3110
|
+
* of the same eye must not be written under it. See \`eyeAxisInWorld\`.
|
|
3111
|
+
*/
|
|
3112
|
+
private writeEyeAxis(model: ArrayLike<number>, skinned: boolean): void {
|
|
3113
|
+
const eye = this.materialModel;
|
|
3114
|
+
if (eye?.kind !== 'eye') return;
|
|
3115
|
+
const at = this.materialField('uModelParams') + 4;
|
|
3116
|
+
const f = this.perFrameFloats;
|
|
3117
|
+
const before0 = f[at];
|
|
3118
|
+
const before1 = f[at + 1];
|
|
3119
|
+
const before2 = f[at + 2];
|
|
3120
|
+
eyeAxisInWorld(eye, model, skinned ? this.skinPaletteData : null, f, at);
|
|
3121
|
+
if (f[at] !== before0 || f[at + 1] !== before1 || f[at + 2] !== before2) this.materials.dirty();
|
|
3122
|
+
}
|
|
3123
|
+
|
|
3124
|
+
/** The views the draw's cloth binds, or the stand-in's where no cloth is set. */
|
|
3125
|
+
private clothViews(): ClothViews {
|
|
3126
|
+
const cloth = this.cloth;
|
|
3127
|
+
return cloth === null ? this.clothStand() : clothViewsOf(cloth.binding, cloth.particles);
|
|
3128
|
+
}
|
|
3129
|
+
|
|
3130
|
+
/** The one-texel stand-in a skinned group binds where there is no cloth, made on first need. */
|
|
3131
|
+
private clothStand(): ClothViews {
|
|
3132
|
+
this.clothStandCache ??= clothStandIn(this.surface.device);
|
|
3133
|
+
return this.clothStandCache.views;
|
|
3134
|
+
}
|
|
3135
|
+
|
|
2935
3136
|
/**
|
|
2936
3137
|
* Choose the morph weights the following `drawMesh` calls deform by, or null for none.
|
|
2937
3138
|
*
|
|
@@ -2955,14 +3156,41 @@ export class WebGPURenderer implements RendererApi {
|
|
|
2955
3156
|
const normal = (material?.normal ?? null) as GpuSurfaceTexture | null;
|
|
2956
3157
|
const orm = (material?.orm ?? null) as GpuSurfaceTexture | null;
|
|
2957
3158
|
const emissiveMap = (material?.emissive ?? null) as GpuSurfaceTexture | null;
|
|
3159
|
+
const modelMap = (material?.modelMap ?? null) as GpuSurfaceTexture | null;
|
|
3160
|
+
this.materialModel = material?.model ?? null;
|
|
3161
|
+
/* What the blur spreads this skin by: the last material to name a profile says. */
|
|
3162
|
+
const model = material?.model;
|
|
3163
|
+
if (model?.kind === 'skin' && this.skinScatter !== null) {
|
|
3164
|
+
this.skinScatter.setProfile(
|
|
3165
|
+
model.profile,
|
|
3166
|
+
model.scatterColor[0] * model.radius,
|
|
3167
|
+
model.scatterColor[1] * model.radius,
|
|
3168
|
+
model.scatterColor[2] * model.radius,
|
|
3169
|
+
);
|
|
3170
|
+
}
|
|
2958
3171
|
this.material((f, i) => {
|
|
2959
|
-
|
|
2960
|
-
|
|
2961
|
-
i[
|
|
3172
|
+
/* The material's four switches, one row: albedo, ORM map, emissive map, two-sided. */
|
|
3173
|
+
const flags = this.materialField('uMaterialFlags');
|
|
3174
|
+
i[flags] = albedo === null ? 0 : 1;
|
|
3175
|
+
/* The cutoff, how this frame resolves a dithered edge, and the frame its pattern moves with. */
|
|
3176
|
+
const cutoff = albedo === null ? 0 : (material?.cutout ?? 0);
|
|
3177
|
+
this.cutoutResolveStaged =
|
|
3178
|
+
cutoff > 0
|
|
3179
|
+
? resolveCutout(
|
|
3180
|
+
material?.cutoutMode ?? 'hard',
|
|
3181
|
+
this.reconstructing || this.temporalJittering,
|
|
3182
|
+
this.samples,
|
|
3183
|
+
)
|
|
3184
|
+
: 'hard';
|
|
3185
|
+
const cut = this.materialField('uCutout');
|
|
3186
|
+
f[cut] = cutoff;
|
|
3187
|
+
f[cut + 1] = CUTOUT_RESOLVE_CODE[this.cutoutResolveStaged];
|
|
3188
|
+
f[cut + 2] = this.cutoutFrame;
|
|
3189
|
+
i[flags + 3] = material?.doubleSided === true ? 1 : 0;
|
|
2962
3190
|
/* 0 is no map, and gates the whole block in the shader. See `uNormalStrength`. */
|
|
2963
3191
|
f[this.materialField('uNormalStrength')] =
|
|
2964
3192
|
normal === null ? 0 : (material?.normalStrength ?? 1);
|
|
2965
|
-
i[
|
|
3193
|
+
i[flags + 1] = orm === null ? 0 : 1;
|
|
2966
3194
|
/*
|
|
2967
3195
|
* Component-aligned with the map — r occlusion, g roughness, b metallic — so this order is
|
|
2968
3196
|
* the channel order rather than the field order of `SurfaceMaterial`. Written component by
|
|
@@ -2973,7 +3201,7 @@ export class WebGPURenderer implements RendererApi {
|
|
|
2973
3201
|
f[scale] = material?.occlusionStrength ?? 1;
|
|
2974
3202
|
f[scale + 1] = material?.roughnessScale ?? 1;
|
|
2975
3203
|
f[scale + 2] = material?.metallicScale ?? 1;
|
|
2976
|
-
i[
|
|
3204
|
+
i[flags + 2] = emissiveMap === null ? 0 : 1;
|
|
2977
3205
|
/*
|
|
2978
3206
|
* One rather than zero on every component, because this multiplies rather than adds: an
|
|
2979
3207
|
* unwritten float is zero, and zero here would switch off every glow the mesh's own vertex
|
|
@@ -2984,6 +3212,9 @@ export class WebGPURenderer implements RendererApi {
|
|
|
2984
3212
|
f[emissive] = asked?.[0] ?? 1;
|
|
2985
3213
|
f[emissive + 1] = asked?.[1] ?? 1;
|
|
2986
3214
|
f[emissive + 2] = asked?.[2] ?? 1;
|
|
3215
|
+
/* The model's numbers and whether its map is bound. See `packModel`. */
|
|
3216
|
+
packModel(material?.model ?? null, modelMap !== null, this.modelParams);
|
|
3217
|
+
f.set(this.modelParams, this.materialField('uModelParams'));
|
|
2987
3218
|
});
|
|
2988
3219
|
/*
|
|
2989
3220
|
* **One lookup for the whole material, and nothing thrown away.**
|
|
@@ -2998,7 +3229,8 @@ export class WebGPURenderer implements RendererApi {
|
|
|
2998
3229
|
this.albedo === albedo &&
|
|
2999
3230
|
this.normalMap === normal &&
|
|
3000
3231
|
this.ormMap === orm &&
|
|
3001
|
-
this.emissiveMap === emissiveMap
|
|
3232
|
+
this.emissiveMap === emissiveMap &&
|
|
3233
|
+
this.modelMap === modelMap
|
|
3002
3234
|
) {
|
|
3003
3235
|
return;
|
|
3004
3236
|
}
|
|
@@ -3006,7 +3238,8 @@ export class WebGPURenderer implements RendererApi {
|
|
|
3006
3238
|
this.normalMap = normal;
|
|
3007
3239
|
this.ormMap = orm;
|
|
3008
3240
|
this.emissiveMap = emissiveMap;
|
|
3009
|
-
this.
|
|
3241
|
+
this.modelMap = modelMap;
|
|
3242
|
+
this.bindGroup = this.flatGroupForMaps(albedo, normal, orm, emissiveMap, modelMap);
|
|
3010
3243
|
}
|
|
3011
3244
|
|
|
3012
3245
|
/**
|
|
@@ -3035,8 +3268,16 @@ export class WebGPURenderer implements RendererApi {
|
|
|
3035
3268
|
normal: GpuSurfaceTexture | null,
|
|
3036
3269
|
orm: GpuSurfaceTexture | null,
|
|
3037
3270
|
emissive: GpuSurfaceTexture | null,
|
|
3271
|
+
/** The model map, a fifth level of the same cache. See `flatBindGroups`. */
|
|
3272
|
+
modelMap: GpuSurfaceTexture | null,
|
|
3038
3273
|
): GPUBindGroup {
|
|
3039
|
-
if (
|
|
3274
|
+
if (
|
|
3275
|
+
albedo === null &&
|
|
3276
|
+
normal === null &&
|
|
3277
|
+
orm === null &&
|
|
3278
|
+
emissive === null &&
|
|
3279
|
+
modelMap === null
|
|
3280
|
+
) {
|
|
3040
3281
|
return this.blankAlbedoBindGroup;
|
|
3041
3282
|
}
|
|
3042
3283
|
let byNormal = this.flatBindGroups.get(albedo);
|
|
@@ -3054,11 +3295,16 @@ export class WebGPURenderer implements RendererApi {
|
|
|
3054
3295
|
byEmissive = new Map();
|
|
3055
3296
|
byOrm.set(orm, byEmissive);
|
|
3056
3297
|
}
|
|
3057
|
-
|
|
3298
|
+
let byModel = byEmissive.get(emissive);
|
|
3299
|
+
if (byModel === undefined) {
|
|
3300
|
+
byModel = new Map();
|
|
3301
|
+
byEmissive.set(emissive, byModel);
|
|
3302
|
+
}
|
|
3303
|
+
const existing = byModel.get(modelMap);
|
|
3058
3304
|
if (existing !== undefined) return existing;
|
|
3059
3305
|
const built = this.buildFlatBindGroup();
|
|
3060
3306
|
this.bindGroupBudget.ask();
|
|
3061
|
-
|
|
3307
|
+
byModel.set(modelMap, built);
|
|
3062
3308
|
return built;
|
|
3063
3309
|
}
|
|
3064
3310
|
|
|
@@ -5287,6 +5533,8 @@ export class WebGPURenderer implements RendererApi {
|
|
|
5287
5533
|
private readonly shadowSkinnedGroups: (GPUBindGroup | null)[] = [];
|
|
5288
5534
|
private readonly shadowSkinnedGroupsBeside: (GPUBindGroup | null)[] = [];
|
|
5289
5535
|
private readonly shadowSkinnedGroupsPalette: (GPUTextureView | null)[] = [];
|
|
5536
|
+
private readonly shadowSkinnedGroupsCloth: (GPUTextureView | null)[] = [];
|
|
5537
|
+
private readonly shadowSkinnedGroupsClothBinding: (GPUTextureView | null)[] = [];
|
|
5290
5538
|
/** Held from construction so the skinned group can be rebuilt without them being threaded. */
|
|
5291
5539
|
private shadowBlankView: GPUTextureView | null = null;
|
|
5292
5540
|
private shadowBlankSampler: GPUSampler | null = null;
|
|
@@ -5425,6 +5673,7 @@ export class WebGPURenderer implements RendererApi {
|
|
|
5425
5673
|
this.shadowDraws.writeFloat(slot, fields.uUvOffset.offset, cutout.uOffset);
|
|
5426
5674
|
this.shadowDraws.writeFloat(slot, fields.uUvOffset.offset + 4, cutout.vOffset);
|
|
5427
5675
|
this.shadowDraws.writeFloat(slot, fields.uAlphaCutout.offset, cutout.cutoff);
|
|
5676
|
+
this.shadowDraws.writeFloat(slot, fields.uAlphaCutout.offset + 4, cutout.dithered);
|
|
5428
5677
|
}
|
|
5429
5678
|
|
|
5430
5679
|
private shadowGroup(): GPUBindGroup {
|
|
@@ -5440,12 +5689,14 @@ export class WebGPURenderer implements RendererApi {
|
|
|
5440
5689
|
* second independently fading occluder can be recorded, and a character is not one — it is the
|
|
5441
5690
|
* thing in front. Serving it would mean a second cached group for a case nothing produces.
|
|
5442
5691
|
*/
|
|
5443
|
-
private shadowSkinnedGroup(palette: GPUTextureView, at: number): GPUBindGroup {
|
|
5692
|
+
private shadowSkinnedGroup(palette: GPUTextureView, at: number, cloth: ClothViews): GPUBindGroup {
|
|
5444
5693
|
const beside = this.shadowBindGroup;
|
|
5445
5694
|
if (
|
|
5446
5695
|
(this.shadowSkinnedGroups[at] ?? null) === null ||
|
|
5447
5696
|
this.shadowSkinnedGroupsBeside[at] !== beside ||
|
|
5448
|
-
this.shadowSkinnedGroupsPalette[at] !== palette
|
|
5697
|
+
this.shadowSkinnedGroupsPalette[at] !== palette ||
|
|
5698
|
+
this.shadowSkinnedGroupsCloth[at] !== cloth.particles ||
|
|
5699
|
+
this.shadowSkinnedGroupsClothBinding[at] !== cloth.binding
|
|
5449
5700
|
) {
|
|
5450
5701
|
this.shadowSkinnedGroups[at] = createDepthBindGroup(
|
|
5451
5702
|
this.surface.device,
|
|
@@ -5455,9 +5706,13 @@ export class WebGPURenderer implements RendererApi {
|
|
|
5455
5706
|
this.shadowBlankView as GPUTextureView,
|
|
5456
5707
|
this.shadowBlankSampler as GPUSampler,
|
|
5457
5708
|
palette,
|
|
5709
|
+
null,
|
|
5710
|
+
cloth,
|
|
5458
5711
|
);
|
|
5459
5712
|
this.shadowSkinnedGroupsBeside[at] = beside;
|
|
5460
5713
|
this.shadowSkinnedGroupsPalette[at] = palette;
|
|
5714
|
+
this.shadowSkinnedGroupsCloth[at] = cloth.particles;
|
|
5715
|
+
this.shadowSkinnedGroupsClothBinding[at] = cloth.binding;
|
|
5461
5716
|
}
|
|
5462
5717
|
return this.shadowSkinnedGroups[at] as GPUBindGroup;
|
|
5463
5718
|
}
|
|
@@ -5496,6 +5751,7 @@ export class WebGPURenderer implements RendererApi {
|
|
|
5496
5751
|
void this.pipelines.enable(feature);
|
|
5497
5752
|
if (this.overlayPipelines !== this.pipelines) void this.overlayPipelines.enable(feature);
|
|
5498
5753
|
if (this.latePipelines !== this.pipelines) void this.latePipelines.enable(feature);
|
|
5754
|
+
if (this.skinPipelines !== null) void this.skinPipelines.enable(feature);
|
|
5499
5755
|
}
|
|
5500
5756
|
/** One resolution every glass caster is read through, so recording one allocates nothing. */
|
|
5501
5757
|
private readonly glassCasterScratch: ResolvedGlass = {
|
|
@@ -5617,7 +5873,7 @@ export class WebGPURenderer implements RendererApi {
|
|
|
5617
5873
|
pass.setIndexBuffer(geometry.indexBuffer, 'uint32');
|
|
5618
5874
|
pass.drawIndexed(geometry.indexCount, count);
|
|
5619
5875
|
},
|
|
5620
|
-
skinnedMesh: (mesh, model, palette, material) => {
|
|
5876
|
+
skinnedMesh: (mesh, model, palette, material, cloth) => {
|
|
5621
5877
|
if (!this.drawingGlassDepth && !castsDepth(material)) {
|
|
5622
5878
|
if (this.quality.glassShadows !== 'off' && glassOf(material, this.glassCasterScratch)) {
|
|
5623
5879
|
this.glassCasters.recordSkinned(mesh, model, palette, material, this.glassCasterScratch);
|
|
@@ -5649,18 +5905,30 @@ export class WebGPURenderer implements RendererApi {
|
|
|
5649
5905
|
|
|
5650
5906
|
const geometry = mesh as GpuMesh & { key?: string };
|
|
5651
5907
|
if (geometry.vertexBuffers === undefined || geometry.indexCount === 0) return;
|
|
5908
|
+
/* A garment casts placed by its cloth, as it draws; `|cloth` keys the pipeline that does. */
|
|
5909
|
+
const clothed = cloth !== undefined;
|
|
5910
|
+
const clothViews = clothed
|
|
5911
|
+
? clothViewsOf(
|
|
5912
|
+
cloth.binding as unknown as GpuClothBinding,
|
|
5913
|
+
cloth.particles as unknown as GpuClothParticles,
|
|
5914
|
+
)
|
|
5915
|
+
: this.clothStand();
|
|
5652
5916
|
pass.setPipeline(
|
|
5653
5917
|
depthPipeline(
|
|
5654
5918
|
this.pipelines,
|
|
5655
5919
|
this.surface.device,
|
|
5656
5920
|
this.shadowSkinnedLayout,
|
|
5657
|
-
|
|
5921
|
+
`${geometry.isSkinnedEight ? 'depth-skinned8' : 'depth-skinned'}${clothed ? '|cloth' : ''}|${this.depthCullMode}|${geometry.key ?? ''}`,
|
|
5658
5922
|
this.presentOf(geometry),
|
|
5659
5923
|
this.depthCullMode,
|
|
5660
5924
|
true,
|
|
5925
|
+
false,
|
|
5926
|
+
false,
|
|
5927
|
+
geometry.isSkinnedEight,
|
|
5928
|
+
clothed,
|
|
5661
5929
|
),
|
|
5662
5930
|
);
|
|
5663
|
-
pass.setBindGroup(0, this.shadowSkinnedGroup(view, paletteSlot), [slot]);
|
|
5931
|
+
pass.setBindGroup(0, this.shadowSkinnedGroup(view, paletteSlot, clothViews), [slot]);
|
|
5664
5932
|
for (let index = 0; index < geometry.vertexBuffers.length; index++) {
|
|
5665
5933
|
pass.setVertexBuffer(index, geometry.vertexBuffers[index] as GPUBuffer);
|
|
5666
5934
|
}
|
|
@@ -6036,6 +6304,26 @@ export class WebGPURenderer implements RendererApi {
|
|
|
6036
6304
|
this.pipelines.format === RECON_HISTORY_FORMAT && this.samples === 1
|
|
6037
6305
|
? this.pipelines
|
|
6038
6306
|
: new PipelineCache(device, RECON_HISTORY_FORMAT, 1, glassShadows, quality.clusteredLights);
|
|
6307
|
+
/*
|
|
6308
|
+
* Screen-space skin needs the composite: linear light to add into, and the depth it resolves.
|
|
6309
|
+
* Without one it is the pre-integrated fit alone, which is the default anyway, and said once.
|
|
6310
|
+
*/
|
|
6311
|
+
const skinScreen = quality.skinScattering === 'screen-space' && quality.screenEffects;
|
|
6312
|
+
if (quality.skinScattering === 'screen-space' && !quality.screenEffects) {
|
|
6313
|
+
console.warn(
|
|
6314
|
+
"WebGPU: skinScattering 'screen-space' needs screenEffects to add into; skin is pre-integrated",
|
|
6315
|
+
);
|
|
6316
|
+
}
|
|
6317
|
+
this.skinScatter = skinScreen ? new SkinScatterPass(device) : null;
|
|
6318
|
+
this.skinPipelines = skinScreen
|
|
6319
|
+
? new PipelineCache(
|
|
6320
|
+
device,
|
|
6321
|
+
SKIN_TARGET_FORMAT,
|
|
6322
|
+
this.samples,
|
|
6323
|
+
glassShadows,
|
|
6324
|
+
quality.clusteredLights,
|
|
6325
|
+
)
|
|
6326
|
+
: null;
|
|
6039
6327
|
this.issueBound = this.issueCommand.bind(this);
|
|
6040
6328
|
this.pipelineTargets =
|
|
6041
6329
|
this.overlayPipelines === this.pipelines
|
|
@@ -6105,7 +6393,7 @@ export class WebGPURenderer implements RendererApi {
|
|
|
6105
6393
|
);
|
|
6106
6394
|
|
|
6107
6395
|
/*
|
|
6108
|
-
* A one-pixel stand-in for the albedo binding.
|
|
6396
|
+
* A one-pixel stand-in for the albedo binding. The albedo's flag is zero for untextured
|
|
6109
6397
|
* geometry, which is nearly all of it, so the shader never samples this — but WebGPU
|
|
6110
6398
|
* requires every declared binding to be filled. WebGL2 keeps an `emptyTexture2D` for
|
|
6111
6399
|
* exactly the same reason.
|
|
@@ -6324,6 +6612,13 @@ export class WebGPURenderer implements RendererApi {
|
|
|
6324
6612
|
? { view: blankSurfaceView, sampler: blankSampler }
|
|
6325
6613
|
: { view: map.view, sampler: map.sampler };
|
|
6326
6614
|
}
|
|
6615
|
+
/* The material's model map, or the stand-in, for the reason `uNormalMap` gives above. */
|
|
6616
|
+
if (name === 'uModelMap') {
|
|
6617
|
+
const map = this.modelMap;
|
|
6618
|
+
return map === null
|
|
6619
|
+
? { view: blankSurfaceView, sampler: blankSampler }
|
|
6620
|
+
: { view: map.view, sampler: map.sampler };
|
|
6621
|
+
}
|
|
6327
6622
|
if (name === 'uOrmMap') {
|
|
6328
6623
|
const map = this.ormMap;
|
|
6329
6624
|
return map === null
|
|
@@ -7231,6 +7526,8 @@ export class WebGPURenderer implements RendererApi {
|
|
|
7231
7526
|
}
|
|
7232
7527
|
/* Not a layout entry of its own — it widens `uvs` — so read from the key beside them. */
|
|
7233
7528
|
present['layers'] = key.includes(':layers');
|
|
7529
|
+
/* Nor is a dynamic mesh's split, which moves positions and normals into buffers of their own. */
|
|
7530
|
+
present['dynamic'] = key.includes(':dynamic');
|
|
7234
7531
|
return present;
|
|
7235
7532
|
}
|
|
7236
7533
|
|
|
@@ -7244,6 +7541,7 @@ export class WebGPURenderer implements RendererApi {
|
|
|
7244
7541
|
*/
|
|
7245
7542
|
beginFrame(clearColor: Vec3): void {
|
|
7246
7543
|
this.cullFrame += 1;
|
|
7544
|
+
this.cutoutFrame = (this.cutoutFrame + 1) % 64;
|
|
7247
7545
|
/* Whatever an update replaced during the last frame; nothing recorded can read it now. */
|
|
7248
7546
|
for (const texture of this.retiredTextures) texture.destroy();
|
|
7249
7547
|
this.retiredTextures.length = 0;
|
|
@@ -7307,6 +7605,8 @@ export class WebGPURenderer implements RendererApi {
|
|
|
7307
7605
|
this.volumeDepthTaken = false;
|
|
7308
7606
|
/* Last frame's snapshot is last frame's scene. */
|
|
7309
7607
|
this.refractSnapshotTaken = false;
|
|
7608
|
+
this.skinScattered = false;
|
|
7609
|
+
this.skinScatter?.reset();
|
|
7310
7610
|
this.plumeVerts.reset();
|
|
7311
7611
|
this.plumeFrags.reset();
|
|
7312
7612
|
this.waterVerts.reset();
|
|
@@ -7489,7 +7789,7 @@ export class WebGPURenderer implements RendererApi {
|
|
|
7489
7789
|
* neither the pipeline set nor the key depends on whether the geometry lands in one call or
|
|
7490
7790
|
* over six frames.
|
|
7491
7791
|
*/
|
|
7492
|
-
private warmFlatPipelines(data: MeshData): string {
|
|
7792
|
+
private warmFlatPipelines(data: MeshData, dynamic: boolean): string {
|
|
7493
7793
|
/*
|
|
7494
7794
|
* **Every optional attribute, not the two that come to mind.** `present` decides where
|
|
7495
7795
|
* each attribute sits in the interleaved buffer, and `createGpuMesh` decides the same
|
|
@@ -7515,6 +7815,15 @@ export class WebGPURenderer implements RendererApi {
|
|
|
7515
7815
|
present['layers'] = true;
|
|
7516
7816
|
key += ':layers';
|
|
7517
7817
|
}
|
|
7818
|
+
/*
|
|
7819
|
+
* A dynamic mesh keeps its positions and normals in buffers of their own (`GpuMesh.update`), so
|
|
7820
|
+
* it binds four buffers where a static one binds two: a different layout, and a key that says
|
|
7821
|
+
* so, or the two would share a pipeline whose buffers one of them does not have.
|
|
7822
|
+
*/
|
|
7823
|
+
if (dynamic) {
|
|
7824
|
+
present['dynamic'] = true;
|
|
7825
|
+
key += ':dynamic';
|
|
7826
|
+
}
|
|
7518
7827
|
const fullKey = `${this.variant}|${key}`;
|
|
7519
7828
|
/*
|
|
7520
7829
|
* **For every target this mesh can land on, which is two when the overlay is its own.**
|
|
@@ -7585,11 +7894,13 @@ export class WebGPURenderer implements RendererApi {
|
|
|
7585
7894
|
* guessing which the scene will use.
|
|
7586
7895
|
*/
|
|
7587
7896
|
const skinnable = data.joints !== undefined;
|
|
7897
|
+
/* An eight-influence rig takes its own skinned pipelines, keyed `|skin8`. See skinning.ts. */
|
|
7898
|
+
const eight = skinnable && data.joints2 !== undefined;
|
|
7588
7899
|
const morphable = data.morphTargets !== undefined;
|
|
7589
7900
|
for (const wantSkin of skinnable ? [false, true] : [false]) {
|
|
7590
7901
|
for (const wantMorph of morphable ? [false, true] : [false]) {
|
|
7591
7902
|
if (!wantSkin && !wantMorph) continue;
|
|
7592
|
-
const suffix = `${wantMorph ? '|morph' : ''}${wantSkin ? '|skin' : ''}`;
|
|
7903
|
+
const suffix = `${wantMorph ? '|morph' : ''}${wantSkin ? (eight ? '|skin8' : '|skin') : ''}`;
|
|
7593
7904
|
for (const translucent of [false, true]) {
|
|
7594
7905
|
void flatPipelineAsync(
|
|
7595
7906
|
pipelines,
|
|
@@ -7601,6 +7912,7 @@ export class WebGPURenderer implements RendererApi {
|
|
|
7601
7912
|
translucent,
|
|
7602
7913
|
wantSkin,
|
|
7603
7914
|
wantMorph,
|
|
7915
|
+
wantSkin && eight,
|
|
7604
7916
|
);
|
|
7605
7917
|
}
|
|
7606
7918
|
}
|
|
@@ -7624,7 +7936,7 @@ export class WebGPURenderer implements RendererApi {
|
|
|
7624
7936
|
}
|
|
7625
7937
|
|
|
7626
7938
|
createMesh(data: MeshData, options: MeshOptions = {}): GpuMesh {
|
|
7627
|
-
const fullKey = this.warmFlatPipelines(data);
|
|
7939
|
+
const fullKey = this.warmFlatPipelines(data, options.dynamic === true);
|
|
7628
7940
|
return this.keyed(createGpuMesh(this.surface.device, data, options.dynamic === true), fullKey);
|
|
7629
7941
|
}
|
|
7630
7942
|
|
|
@@ -7638,7 +7950,7 @@ export class WebGPURenderer implements RendererApi {
|
|
|
7638
7950
|
* can do nothing about. Done with `complete` still false is the signal that it was abandoned.
|
|
7639
7951
|
*/
|
|
7640
7952
|
createMeshIncremental(data: MeshData, options: MeshOptions = {}): IncrementalMeshHandle {
|
|
7641
|
-
const fullKey = this.warmFlatPipelines(data);
|
|
7953
|
+
const fullKey = this.warmFlatPipelines(data, options.dynamic === true);
|
|
7642
7954
|
const { mesh, upload } = createGpuMeshIncremental(
|
|
7643
7955
|
this.surface.device,
|
|
7644
7956
|
data,
|
|
@@ -7659,20 +7971,20 @@ export class WebGPURenderer implements RendererApi {
|
|
|
7659
7971
|
/**
|
|
7660
7972
|
* Rewrite a mesh's positions, and its normals where the caller has them.
|
|
7661
7973
|
*
|
|
7662
|
-
* Refused in words on a mesh that did not declare itself dynamic, because
|
|
7663
|
-
*
|
|
7664
|
-
*
|
|
7974
|
+
* Refused in words on a mesh that did not declare itself dynamic, because what makes an update
|
|
7975
|
+
* cheap on this backend — positions and normals in buffers of their own — is a layout chosen at
|
|
7976
|
+
* creation. See `GpuMesh.update`.
|
|
7665
7977
|
*/
|
|
7666
7978
|
updateMesh(mesh: GpuMesh, positions: Float32Array, normals?: Float32Array): void {
|
|
7667
7979
|
if (mesh.update === null) {
|
|
7668
7980
|
throw new Error(
|
|
7669
|
-
'this mesh was not created with { dynamic: true }, so
|
|
7670
|
-
'
|
|
7671
|
-
'
|
|
7981
|
+
'this mesh was not created with { dynamic: true }, so its positions share one buffer with ' +
|
|
7982
|
+
'every other attribute and cannot be rewritten alone. Say so at `createMesh` — the flag ' +
|
|
7983
|
+
'decides the layout, and what a buffer is hinted as.',
|
|
7672
7984
|
);
|
|
7673
7985
|
}
|
|
7674
7986
|
/*
|
|
7675
|
-
* **The
|
|
7987
|
+
* **The positions before this rewrite are last frame's**, kept only for a reconstruction and only
|
|
7676
7988
|
* on the first rewrite of a frame — a second must not replace where the vertices were last
|
|
7677
7989
|
* frame with where they were a moment ago. A rewrite between frames is the next frame's. See
|
|
7678
7990
|
* `changeFrames.ts`.
|
|
@@ -7806,7 +8118,11 @@ export class WebGPURenderer implements RendererApi {
|
|
|
7806
8118
|
i[at('uLightingEnabled')] = 1;
|
|
7807
8119
|
i[at('uFogEnabled')] = FOG_RECEDE;
|
|
7808
8120
|
this.surfaceFog = FOG_RECEDE;
|
|
7809
|
-
|
|
8121
|
+
/* Every material switch off — albedo, ORM map, emissive map, two-sided — as WebGL2 resets them. */
|
|
8122
|
+
i.fill(0, at('uMaterialFlags'), at('uMaterialFlags') + 4);
|
|
8123
|
+
f[at('uCutout')] = 0;
|
|
8124
|
+
f[at('uCutout') + 1] = 0;
|
|
8125
|
+
this.cutoutResolveStaged = 'hard';
|
|
7810
8126
|
this.materialHasMaps = false;
|
|
7811
8127
|
|
|
7812
8128
|
/*
|
|
@@ -7952,7 +8268,7 @@ export class WebGPURenderer implements RendererApi {
|
|
|
7952
8268
|
/*
|
|
7953
8269
|
* The ORM scales, whose base is 1 and not 0 — the value an unwritten uniform would hold.
|
|
7954
8270
|
*
|
|
7955
|
-
*
|
|
8271
|
+
* The ORM map's flag is zeroed with the others above, and zero is exactly "no map".
|
|
7956
8272
|
* These three are the opposite case, the one this block's own header is about: a scale of
|
|
7957
8273
|
* zero is a real and wrong value, so leaving them silent would not switch the map off, it
|
|
7958
8274
|
* would read every channel as its floor the moment one was bound.
|
|
@@ -8582,10 +8898,18 @@ export class WebGPURenderer implements RendererApi {
|
|
|
8582
8898
|
* before last. That is the defined state the two-backends rule asks for.
|
|
8583
8899
|
*/
|
|
8584
8900
|
const refracting = this.bindSeeThrough(options);
|
|
8901
|
+
/* A dithered cutout tests hard on a blended draw, which has nothing to average a pattern with;
|
|
8902
|
+
put back below with the rest. See `cutoutDither.ts`. */
|
|
8903
|
+
const ditheredBlend = blend && this.cutoutResolveStaged !== 'hard';
|
|
8904
|
+
if (ditheredBlend) this.perFrameFloats[this.materialField('uCutout') + 1] = 0;
|
|
8585
8905
|
/* Taken for itself when any of those differs from the pass, and put back below: the rule both
|
|
8586
8906
|
backends count material changes by. See `materialChanges.ts`. */
|
|
8587
|
-
const own =
|
|
8907
|
+
const own =
|
|
8908
|
+
ownsMaterial({ opacity, lit, fog, toneMapped, refracting }, this.surfaceFog) || ditheredBlend;
|
|
8588
8909
|
if (own) this.materials.dirty();
|
|
8910
|
+
/* An eye's axis is the draw's, written into the open material. See `eyeAxisInWorld`. */
|
|
8911
|
+
const modelKind = this.materialModel?.kind ?? null;
|
|
8912
|
+
if (modelKind === 'eye') this.writeEyeAxis(model, this.skinPaletteSlot >= 0 && mesh.isSkinned);
|
|
8589
8913
|
const base = (mesh as GpuMesh & { key?: string }).key ?? 'flat:s0:u0';
|
|
8590
8914
|
/*
|
|
8591
8915
|
* A rigged mesh takes the skinned vertex variant, an unrigged one the plain variant, and the
|
|
@@ -8597,7 +8921,13 @@ export class WebGPURenderer implements RendererApi {
|
|
|
8597
8921
|
const skinned = this.skinPaletteSlot >= 0 && mesh.isSkinned;
|
|
8598
8922
|
const morphed = this.morphWeights !== null && mesh.morph !== null;
|
|
8599
8923
|
const deltas = morphed && mesh.morph !== null ? mesh.morph.view() : null;
|
|
8600
|
-
const
|
|
8924
|
+
const eight = skinned && mesh.isSkinnedEight;
|
|
8925
|
+
/* A cloth set and a skinned draw: the pipeline built with CLOTH_BOUND on, warmed when the
|
|
8926
|
+
binding was made. See `createClothBinding`. */
|
|
8927
|
+
const clothed = skinned && this.cloth !== null;
|
|
8928
|
+
const keyed =
|
|
8929
|
+
`${base}${morphed ? '|morph' : ''}${skinned ? (eight ? '|skin8' : '|skin') : ''}` +
|
|
8930
|
+
`${clothed ? '|cloth' : ''}`;
|
|
8601
8931
|
/*
|
|
8602
8932
|
* Depth writing and the overlay layer are pipeline state on this backend, so they belong in
|
|
8603
8933
|
* the key: two draws of one mesh differing only in these would otherwise share a cached
|
|
@@ -8612,6 +8942,9 @@ export class WebGPURenderer implements RendererApi {
|
|
|
8612
8942
|
const layer = Math.min(Math.max(Math.round(options.depthLayer ?? 0), 0), MAX_DEPTH_LAYER);
|
|
8613
8943
|
/* Added rather than blended over: its own blend state, so its own pipeline. */
|
|
8614
8944
|
const adds = blend && options.additive === true;
|
|
8945
|
+
const coverage = !blend && this.cutoutResolveStaged === 'coverage';
|
|
8946
|
+
/* Skin under the screen-space blur draws its two halves; see `keepSkinDiffuse`. */
|
|
8947
|
+
const splits = modelKind === 'skin' && !blend && this.skinSplits();
|
|
8615
8948
|
const key =
|
|
8616
8949
|
`${keyed}${blend ? (adds ? '|add' : '|blend') : ''}` +
|
|
8617
8950
|
`${depthWrite ? '' : '|nodepth'}${layer > 0 ? `|dl${layer}` : ''}` +
|
|
@@ -8619,7 +8952,13 @@ export class WebGPURenderer implements RendererApi {
|
|
|
8619
8952
|
one must never share a cached pipeline with the same mesh drawn into the other. */
|
|
8620
8953
|
`${this.oitMode === 'none' ? '' : `|oit${this.oitMode}`}` +
|
|
8621
8954
|
/* Culling is pipeline state here: a two-sided surface culls nothing. */
|
|
8622
|
-
`${this.materialDoubleSided ? '|2s' : ''}
|
|
8955
|
+
`${this.materialDoubleSided ? '|2s' : ''}` +
|
|
8956
|
+
/* And so is alpha-to-coverage, for a dithered cutout in a multisampled frame. */
|
|
8957
|
+
`${coverage ? '|a2c' : ''}` +
|
|
8958
|
+
/* And the material's shading model, which is the fragment stage's own switch. */
|
|
8959
|
+
`${modelKind === null ? '' : `|m:${modelKind}`}` +
|
|
8960
|
+
/* And which half of a skin it draws, under the screen-space blur. */
|
|
8961
|
+
`${splits ? '|ss' : ''}`;
|
|
8623
8962
|
|
|
8624
8963
|
/*
|
|
8625
8964
|
* The morph uniforms, written only for a morphed draw and read from the *morphed* variant's
|
|
@@ -8665,6 +9004,11 @@ export class WebGPURenderer implements RendererApi {
|
|
|
8665
9004
|
false,
|
|
8666
9005
|
this.oitMode,
|
|
8667
9006
|
this.materialDoubleSided,
|
|
9007
|
+
coverage,
|
|
9008
|
+
eight,
|
|
9009
|
+
clothed,
|
|
9010
|
+
modelKind,
|
|
9011
|
+
splits ? 'scene' : 'whole',
|
|
8668
9012
|
);
|
|
8669
9013
|
}
|
|
8670
9014
|
|
|
@@ -8718,6 +9062,21 @@ export class WebGPURenderer implements RendererApi {
|
|
|
8718
9062
|
pass.setIndexBuffer(mesh.indexBuffer, 'uint32');
|
|
8719
9063
|
pass.drawIndexed(mesh.indexCount);
|
|
8720
9064
|
}
|
|
9065
|
+
if (splits) {
|
|
9066
|
+
this.keepSkinDiffuse(
|
|
9067
|
+
mesh,
|
|
9068
|
+
key,
|
|
9069
|
+
base,
|
|
9070
|
+
skinned,
|
|
9071
|
+
morphed,
|
|
9072
|
+
deltas,
|
|
9073
|
+
layer,
|
|
9074
|
+
eight,
|
|
9075
|
+
clothed,
|
|
9076
|
+
slot,
|
|
9077
|
+
material,
|
|
9078
|
+
);
|
|
9079
|
+
}
|
|
8721
9080
|
}
|
|
8722
9081
|
/* Back to the defaults for whatever is drawn next, exactly as `renderer.ts` restores them —
|
|
8723
9082
|
unconditionally, because the scratch state above is shared by every draw after this one
|
|
@@ -8736,6 +9095,10 @@ export class WebGPURenderer implements RendererApi {
|
|
|
8736
9095
|
if (!toneMapped) {
|
|
8737
9096
|
this.perFrameInts[this.materialField('uOutputTransform')] = this.gradeCode();
|
|
8738
9097
|
}
|
|
9098
|
+
if (ditheredBlend) {
|
|
9099
|
+
this.perFrameFloats[this.materialField('uCutout') + 1] =
|
|
9100
|
+
CUTOUT_RESOLVE_CODE[this.cutoutResolveStaged];
|
|
9101
|
+
}
|
|
8739
9102
|
if (own) this.materials.dirty();
|
|
8740
9103
|
}
|
|
8741
9104
|
|
|
@@ -8999,8 +9362,16 @@ export class WebGPURenderer implements RendererApi {
|
|
|
8999
9362
|
const layer = Math.min(Math.max(Math.round(options.depthLayer ?? 0), 0), MAX_DEPTH_LAYER);
|
|
9000
9363
|
/* A blended batch shows what is behind it — refraction or glass — as a single draw does. */
|
|
9001
9364
|
const refracting = blend && this.bindSeeThrough(options);
|
|
9002
|
-
|
|
9365
|
+
/* As a single draw: a blended batch tests a dithered cutout hard, an opaque one in a
|
|
9366
|
+
multisampled frame takes coverage. See `cutoutDither.ts`. */
|
|
9367
|
+
const ditheredBlend = blend && this.cutoutResolveStaged !== 'hard';
|
|
9368
|
+
if (ditheredBlend) this.perFrameFloats[this.materialField('uCutout') + 1] = 0;
|
|
9369
|
+
const coverage = !blend && this.cutoutResolveStaged === 'coverage';
|
|
9370
|
+
const own =
|
|
9371
|
+
ownsMaterial({ opacity, lit, fog, toneMapped, refracting }, this.surfaceFog) || ditheredBlend;
|
|
9003
9372
|
if (own) this.materials.dirty();
|
|
9373
|
+
/* A batch shades by its material's model too; an eye's axis is a placement's, not a batch's. */
|
|
9374
|
+
const modelKind = this.materialModel?.kind ?? null;
|
|
9004
9375
|
const base = (mesh as GpuMesh & { key?: string }).key ?? 'flat:s0:u0';
|
|
9005
9376
|
/* `|inst` in the key, for the reason every other suffix is there: an instanced pipeline binds
|
|
9006
9377
|
a third vertex buffer and a different vertex module, and sharing a cache entry with the
|
|
@@ -9009,7 +9380,8 @@ export class WebGPURenderer implements RendererApi {
|
|
|
9009
9380
|
const key =
|
|
9010
9381
|
`${base}|inst${blend ? (adds ? '|add' : '|blend') : ''}` +
|
|
9011
9382
|
`${depthWrite ? '' : '|nodepth'}${layer > 0 ? `|dl${layer}` : ''}` +
|
|
9012
|
-
`${this.materialDoubleSided ? '|2s' : ''}
|
|
9383
|
+
`${this.materialDoubleSided ? '|2s' : ''}${coverage ? '|a2c' : ''}` +
|
|
9384
|
+
`${modelKind === null ? '' : `|m:${modelKind}`}`;
|
|
9013
9385
|
|
|
9014
9386
|
const pipelines = blend ? this.blendedPipelines(lands) : this.targetPipelines();
|
|
9015
9387
|
let pipeline = pipelines.peek(key);
|
|
@@ -9033,6 +9405,10 @@ export class WebGPURenderer implements RendererApi {
|
|
|
9033
9405
|
true,
|
|
9034
9406
|
'none',
|
|
9035
9407
|
this.materialDoubleSided,
|
|
9408
|
+
coverage,
|
|
9409
|
+
false,
|
|
9410
|
+
false,
|
|
9411
|
+
modelKind,
|
|
9036
9412
|
);
|
|
9037
9413
|
}
|
|
9038
9414
|
|
|
@@ -9087,6 +9463,10 @@ export class WebGPURenderer implements RendererApi {
|
|
|
9087
9463
|
this.perFrameInts[this.materialField('uOutputTransform')] = this.gradeCode();
|
|
9088
9464
|
}
|
|
9089
9465
|
if (refracting) this.releaseSeeThrough();
|
|
9466
|
+
if (ditheredBlend) {
|
|
9467
|
+
this.perFrameFloats[this.materialField('uCutout') + 1] =
|
|
9468
|
+
CUTOUT_RESOLVE_CODE[this.cutoutResolveStaged];
|
|
9469
|
+
}
|
|
9090
9470
|
if (own) this.materials.dirty();
|
|
9091
9471
|
}
|
|
9092
9472
|
|
|
@@ -9324,12 +9704,19 @@ export class WebGPURenderer implements RendererApi {
|
|
|
9324
9704
|
this.bindSceneCasterMaterial(material);
|
|
9325
9705
|
this.drawMesh(mesh as GpuMesh, model);
|
|
9326
9706
|
},
|
|
9327
|
-
skinnedMesh: (mesh, model, palette, material) => {
|
|
9707
|
+
skinnedMesh: (mesh, model, palette, material, cloth) => {
|
|
9328
9708
|
this.bindSceneCasterMaterial(material);
|
|
9329
9709
|
this.setSkinPalette(palette);
|
|
9710
|
+
if (cloth !== undefined) {
|
|
9711
|
+
this.setCloth(
|
|
9712
|
+
cloth.binding as unknown as GpuClothBinding,
|
|
9713
|
+
cloth.particles as unknown as GpuClothParticles,
|
|
9714
|
+
);
|
|
9715
|
+
}
|
|
9330
9716
|
this.drawMesh(mesh as GpuMesh, model);
|
|
9331
9717
|
/* Put back, or the next rigid draw in the replay skins by whatever this one left bound. */
|
|
9332
9718
|
this.setSkinPalette(null);
|
|
9719
|
+
this.setCloth(null);
|
|
9333
9720
|
},
|
|
9334
9721
|
instanced: (batch, data, material) => {
|
|
9335
9722
|
this.bindSceneCasterMaterial(material);
|
|
@@ -10237,11 +10624,15 @@ export class WebGPURenderer implements RendererApi {
|
|
|
10237
10624
|
/* Slot `k` caches at `k + 1`; index 0 is the morphed draw that is not skinned and has no
|
|
10238
10625
|
slot of its own. See `skinnedGroups`. */
|
|
10239
10626
|
const at = skinned ? this.skinPaletteSlot + 1 : 0;
|
|
10627
|
+
/* A skinned group carries the cloth's views, or the stand-in's: see `clothViews`. */
|
|
10628
|
+
const cloth = skinned ? this.clothViews() : null;
|
|
10240
10629
|
if (
|
|
10241
10630
|
(this.skinnedGroups[at] ?? null) === null ||
|
|
10242
10631
|
this.skinnedGroupsBeside[at] !== this.bindGroup ||
|
|
10243
10632
|
this.skinnedGroupsPalette[at] !== palette ||
|
|
10244
|
-
this.skinnedGroupsDeltas[at] !== deltas
|
|
10633
|
+
this.skinnedGroupsDeltas[at] !== deltas ||
|
|
10634
|
+
this.skinnedGroupsCloth[at] !== (cloth?.particles ?? null) ||
|
|
10635
|
+
this.skinnedGroupsClothBinding[at] !== (cloth?.binding ?? null)
|
|
10245
10636
|
) {
|
|
10246
10637
|
this.skinnedGroups[at] = createFlatBindGroup(
|
|
10247
10638
|
this.surface.device,
|
|
@@ -10252,10 +10643,13 @@ export class WebGPURenderer implements RendererApi {
|
|
|
10252
10643
|
this.flatTextures,
|
|
10253
10644
|
palette,
|
|
10254
10645
|
deltas,
|
|
10646
|
+
cloth,
|
|
10255
10647
|
);
|
|
10256
10648
|
this.skinnedGroupsBeside[at] = this.bindGroup;
|
|
10257
10649
|
this.skinnedGroupsPalette[at] = palette;
|
|
10258
10650
|
this.skinnedGroupsDeltas[at] = deltas;
|
|
10651
|
+
this.skinnedGroupsCloth[at] = cloth?.particles ?? null;
|
|
10652
|
+
this.skinnedGroupsClothBinding[at] = cloth?.binding ?? null;
|
|
10259
10653
|
}
|
|
10260
10654
|
return this.skinnedGroups[at] as GPUBindGroup;
|
|
10261
10655
|
}
|
|
@@ -10324,21 +10718,25 @@ export class WebGPURenderer implements RendererApi {
|
|
|
10324
10718
|
const normalMap = this.normalMap;
|
|
10325
10719
|
const ormMap = this.ormMap;
|
|
10326
10720
|
const emissiveMap = this.emissiveMap;
|
|
10721
|
+
const modelMap = this.modelMap;
|
|
10327
10722
|
this.albedo = null;
|
|
10328
10723
|
this.normalMap = null;
|
|
10329
10724
|
this.ormMap = null;
|
|
10330
10725
|
this.emissiveMap = null;
|
|
10726
|
+
this.modelMap = null;
|
|
10331
10727
|
this.blankAlbedoBindGroup = this.buildFlatBindGroup();
|
|
10332
10728
|
this.albedo = albedo;
|
|
10333
10729
|
this.normalMap = normalMap;
|
|
10334
10730
|
this.ormMap = ormMap;
|
|
10335
10731
|
this.emissiveMap = emissiveMap;
|
|
10732
|
+
this.modelMap = modelMap;
|
|
10336
10733
|
|
|
10337
10734
|
this.bindGroup = this.flatGroupForMaps(
|
|
10338
10735
|
this.albedo,
|
|
10339
10736
|
this.normalMap,
|
|
10340
10737
|
this.ormMap,
|
|
10341
10738
|
this.emissiveMap,
|
|
10739
|
+
this.modelMap,
|
|
10342
10740
|
);
|
|
10343
10741
|
/* The open material lives in a slot of a buffer that no longer exists. */
|
|
10344
10742
|
this.materials.dirty();
|
|
@@ -10381,7 +10779,8 @@ export class WebGPURenderer implements RendererApi {
|
|
|
10381
10779
|
this.albedo === null &&
|
|
10382
10780
|
this.normalMap === null &&
|
|
10383
10781
|
this.ormMap === null &&
|
|
10384
|
-
this.emissiveMap === null
|
|
10782
|
+
this.emissiveMap === null &&
|
|
10783
|
+
this.modelMap === null
|
|
10385
10784
|
) {
|
|
10386
10785
|
this.blankAlbedoBindGroup = this.buildFlatBindGroup();
|
|
10387
10786
|
this.bindGroupBudget.ask();
|
|
@@ -10393,6 +10792,7 @@ export class WebGPURenderer implements RendererApi {
|
|
|
10393
10792
|
this.normalMap,
|
|
10394
10793
|
this.ormMap,
|
|
10395
10794
|
this.emissiveMap,
|
|
10795
|
+
this.modelMap,
|
|
10396
10796
|
);
|
|
10397
10797
|
}
|
|
10398
10798
|
|
|
@@ -11125,6 +11525,8 @@ export class WebGPURenderer implements RendererApi {
|
|
|
11125
11525
|
* the only moment they can land; everything else keeps the path it has.
|
|
11126
11526
|
*/
|
|
11127
11527
|
private recordBlended(reads: number, lands = this.drawsLate()): DrawCommand | null {
|
|
11528
|
+
/* Skin is spread and added before anything blended lands over it. See `runSkinScatter`. */
|
|
11529
|
+
if (this.skinScatter !== null && !this.skinScattered) this.runSkinScatter();
|
|
11128
11530
|
const late = this.latePass;
|
|
11129
11531
|
if (late !== null && lands) return late.take();
|
|
11130
11532
|
return this.recordDraw(reads, this.currentTarget());
|
|
@@ -11212,6 +11614,16 @@ export class WebGPURenderer implements RendererApi {
|
|
|
11212
11614
|
const dynamic = mesh.motion;
|
|
11213
11615
|
const rows = dynamic?.previous ?? null;
|
|
11214
11616
|
const rewritten = rows !== null && dynamic !== null && dynamic.changed === this.moverFrame;
|
|
11617
|
+
const cloth = this.cloth;
|
|
11618
|
+
if (
|
|
11619
|
+
cloth !== null &&
|
|
11620
|
+
this.skinPaletteSlot >= 0 &&
|
|
11621
|
+
mesh.isSkinned &&
|
|
11622
|
+
mesh.skinOffsets !== null
|
|
11623
|
+
) {
|
|
11624
|
+
this.recordClothMotion(mesh, model, previous, cloth);
|
|
11625
|
+
return;
|
|
11626
|
+
}
|
|
11215
11627
|
if (previous === null) {
|
|
11216
11628
|
if (rewritten) this.motionPass?.recordDynamic(mesh, model, model, rows);
|
|
11217
11629
|
return;
|
|
@@ -11258,6 +11670,58 @@ export class WebGPURenderer implements RendererApi {
|
|
|
11258
11670
|
this.motionPass?.recordSkinned(mesh, model, mover.previousModel, now, was);
|
|
11259
11671
|
}
|
|
11260
11672
|
|
|
11673
|
+
/**
|
|
11674
|
+
* A cloth-bound draw's motion. **Recorded even where the caller named no previous place**, as a
|
|
11675
|
+
* rewritten mesh's is: the particles carry their own past, and a garment swinging on a character
|
|
11676
|
+
* standing still is motion nobody else can see. Where the rig is a `Mover` its last pose is taken
|
|
11677
|
+
* as a skinned mover's is; otherwise the rig is taken not to have moved.
|
|
11678
|
+
*/
|
|
11679
|
+
private recordClothMotion(
|
|
11680
|
+
mesh: GpuMesh,
|
|
11681
|
+
model: ArrayLike<number>,
|
|
11682
|
+
previous: ArrayLike<number> | Mover | null,
|
|
11683
|
+
cloth: { binding: GpuClothBinding; particles: GpuClothParticles },
|
|
11684
|
+
): void {
|
|
11685
|
+
const now = this.skinPalettes.view(this.skinPaletteSlot);
|
|
11686
|
+
if (now === null) return;
|
|
11687
|
+
let was: GPUTextureView | null = now;
|
|
11688
|
+
let wasModel: ArrayLike<number> = model;
|
|
11689
|
+
if (previous !== null && isMover(previous)) {
|
|
11690
|
+
const verdict = advanceMover(
|
|
11691
|
+
previous,
|
|
11692
|
+
this.moverRenderer,
|
|
11693
|
+
this.moverFrame,
|
|
11694
|
+
model,
|
|
11695
|
+
this.skinPaletteData,
|
|
11696
|
+
);
|
|
11697
|
+
if (verdict === MOVER_TWICE) {
|
|
11698
|
+
this.warnMoverTwice();
|
|
11699
|
+
return;
|
|
11700
|
+
}
|
|
11701
|
+
if (verdict === MOVER_MOTION) {
|
|
11702
|
+
const slot = this.skinPalettes.take(this.surface.device, previous.previousPalette);
|
|
11703
|
+
was = slot === null ? null : this.skinPalettes.view(slot);
|
|
11704
|
+
wasModel = previous.previousModel;
|
|
11705
|
+
}
|
|
11706
|
+
} else if (previous !== null) {
|
|
11707
|
+
wasModel = previous;
|
|
11708
|
+
}
|
|
11709
|
+
if (was === null) return;
|
|
11710
|
+
const particlesWas = cloth.particles.previousAt(this.moverFrame);
|
|
11711
|
+
/* Nothing said it moved and nothing did: the camera's motion, derived from depth, is the answer. */
|
|
11712
|
+
if (previous === null && particlesWas === cloth.particles.current) return;
|
|
11713
|
+
this.motionPass?.recordCloth(
|
|
11714
|
+
mesh,
|
|
11715
|
+
model,
|
|
11716
|
+
wasModel,
|
|
11717
|
+
now,
|
|
11718
|
+
was,
|
|
11719
|
+
cloth.binding.bindingView,
|
|
11720
|
+
cloth.particles.current,
|
|
11721
|
+
particlesWas,
|
|
11722
|
+
);
|
|
11723
|
+
}
|
|
11724
|
+
|
|
11261
11725
|
private warnMoverTwice(): void {
|
|
11262
11726
|
if (this.warnedMoverTwice) return;
|
|
11263
11727
|
this.warnedMoverTwice = true;
|
|
@@ -12347,6 +12811,150 @@ export class WebGPURenderer implements RendererApi {
|
|
|
12347
12811
|
return true;
|
|
12348
12812
|
}
|
|
12349
12813
|
|
|
12814
|
+
/**
|
|
12815
|
+
* Whether a skin draw now draws its two halves: the profile asked, the frame has not spread its
|
|
12816
|
+
* skin yet, and this is the frame's own picture — not a mirror's, a probe's, an overlay after
|
|
12817
|
+
* the frame, or an order-independent set, none of which the spread reads.
|
|
12818
|
+
*/
|
|
12819
|
+
private skinSplits(): boolean {
|
|
12820
|
+
return (
|
|
12821
|
+
this.skinScatter !== null &&
|
|
12822
|
+
!this.skinScattered &&
|
|
12823
|
+
!this.reflectionPassActive &&
|
|
12824
|
+
!this.probePassActive &&
|
|
12825
|
+
!this.framePresented &&
|
|
12826
|
+
!this.overlayActive &&
|
|
12827
|
+
this.oitMode === 'none'
|
|
12828
|
+
);
|
|
12829
|
+
}
|
|
12830
|
+
|
|
12831
|
+
/**
|
|
12832
|
+
* A skin draw's diffuse half, kept for the spread: the frame's draw again, with the same slots,
|
|
12833
|
+
* buffers and bind group, through the pipeline that writes the diffuse alone into the blur's
|
|
12834
|
+
* target and tests for the depth its frame half wrote.
|
|
12835
|
+
*/
|
|
12836
|
+
private keepSkinDiffuse(
|
|
12837
|
+
mesh: GpuMesh,
|
|
12838
|
+
frameKey: string,
|
|
12839
|
+
base: string,
|
|
12840
|
+
skinned: boolean,
|
|
12841
|
+
morphed: boolean,
|
|
12842
|
+
deltas: GPUTextureView | null,
|
|
12843
|
+
layer: number,
|
|
12844
|
+
eight: boolean,
|
|
12845
|
+
clothed: boolean,
|
|
12846
|
+
slot: number,
|
|
12847
|
+
material: number,
|
|
12848
|
+
): void {
|
|
12849
|
+
const skin = this.skinScatter;
|
|
12850
|
+
const pipelines = this.skinPipelines;
|
|
12851
|
+
const present = this.meshPresent.get(base);
|
|
12852
|
+
if (skin === null || pipelines === null || present === undefined) return;
|
|
12853
|
+
const key = `${frameKey}|sd`;
|
|
12854
|
+
const pipeline =
|
|
12855
|
+
pipelines.peek(key) ??
|
|
12856
|
+
flatPipeline(
|
|
12857
|
+
pipelines,
|
|
12858
|
+
this.surface.device,
|
|
12859
|
+
this.flatLayoutFor(skinned, morphed),
|
|
12860
|
+
this.variant,
|
|
12861
|
+
key,
|
|
12862
|
+
present,
|
|
12863
|
+
false,
|
|
12864
|
+
skinned,
|
|
12865
|
+
morphed,
|
|
12866
|
+
true,
|
|
12867
|
+
layer,
|
|
12868
|
+
false,
|
|
12869
|
+
'none',
|
|
12870
|
+
this.materialDoubleSided,
|
|
12871
|
+
false,
|
|
12872
|
+
eight,
|
|
12873
|
+
clothed,
|
|
12874
|
+
'skin',
|
|
12875
|
+
'diffuse',
|
|
12876
|
+
);
|
|
12877
|
+
const command = skin.take();
|
|
12878
|
+
command.pipeline = pipeline;
|
|
12879
|
+
command.bindGroup = this.flatBindGroupFor(skinned, morphed, deltas);
|
|
12880
|
+
command.offsetA = slot;
|
|
12881
|
+
command.offsetB = material;
|
|
12882
|
+
command.offsetCount = 2;
|
|
12883
|
+
for (let index = 0; index < mesh.vertexBuffers.length; index++) {
|
|
12884
|
+
command.vertexBuffers[index] = mesh.vertexBuffers[index] as GPUBuffer;
|
|
12885
|
+
}
|
|
12886
|
+
command.vertexCount = mesh.vertexBuffers.length;
|
|
12887
|
+
command.indexBuffer = mesh.indexBuffer;
|
|
12888
|
+
command.indexed = true;
|
|
12889
|
+
command.count = mesh.indexCount;
|
|
12890
|
+
command.instances = 1;
|
|
12891
|
+
command.indirect = null;
|
|
12892
|
+
}
|
|
12893
|
+
|
|
12894
|
+
/**
|
|
12895
|
+
* Spread the frame's skin and add it back: once, at the first blended draw or before the frame's
|
|
12896
|
+
* last flush. **A fifth instance of the boundary `takeRefractSnapshot` describes** — flush, end
|
|
12897
|
+
* the pass, work between passes, reopen with `loadOp: 'load'` — and refused where that one is,
|
|
12898
|
+
* for its reasons. Between the passes: the kept diffuse halves into their target against the
|
|
12899
|
+
* frame's depth, the depth resolved, and `SkinScatterPass.spread` across and then down into the
|
|
12900
|
+
* frame's colour.
|
|
12901
|
+
*/
|
|
12902
|
+
private runSkinScatter(): void {
|
|
12903
|
+
const skin = this.skinScatter;
|
|
12904
|
+
if (skin === null || this.skinScattered) return;
|
|
12905
|
+
/* Latched whether or not skin was kept: a skin drawn after this point is the whole surface. */
|
|
12906
|
+
this.skinScattered = true;
|
|
12907
|
+
if (!skin.pending) return;
|
|
12908
|
+
if (this.reflectionPassActive || this.probePassActive || this.oitReplaying) return;
|
|
12909
|
+
const encoder = this.encoder;
|
|
12910
|
+
const depth = this.depthView;
|
|
12911
|
+
const resolved = this.resolvedDepthView;
|
|
12912
|
+
const target = this.compositeTarget();
|
|
12913
|
+
const projection = this.frameProjection;
|
|
12914
|
+
if (encoder === null || depth === null || resolved === null) return;
|
|
12915
|
+
if (target === null || projection === null) return;
|
|
12916
|
+
if (this.pass === null && this.openPass() === null) return;
|
|
12917
|
+
if (this.pass === null) return;
|
|
12918
|
+
if (this.quality.frameGraph) this.flushGraph();
|
|
12919
|
+
|
|
12920
|
+
this.pass.end();
|
|
12921
|
+
const size = this.sceneSize();
|
|
12922
|
+
skin.size(size.width, size.height, this.samples);
|
|
12923
|
+
skin.drawDiffuse(encoder, depth, this.issueBound, this.gpuTimer.writesFor());
|
|
12924
|
+
this.resolveDepth(encoder);
|
|
12925
|
+
mat4.invert(this.skinInverseProjection, projection);
|
|
12926
|
+
const multisampled = this.compositeMsaa();
|
|
12927
|
+
skin.spread(
|
|
12928
|
+
encoder,
|
|
12929
|
+
this.pipelines,
|
|
12930
|
+
resolved,
|
|
12931
|
+
multisampled ?? target,
|
|
12932
|
+
multisampled === null ? null : target,
|
|
12933
|
+
this.samples,
|
|
12934
|
+
projection,
|
|
12935
|
+
this.skinInverseProjection,
|
|
12936
|
+
() => this.gpuTimer.writesFor(),
|
|
12937
|
+
);
|
|
12938
|
+
this.pass = encoder.beginRenderPass({
|
|
12939
|
+
label: 'skin.scattered',
|
|
12940
|
+
timestampWrites: this.gpuTimer.writesFor(),
|
|
12941
|
+
colorAttachments: [
|
|
12942
|
+
{
|
|
12943
|
+
view: multisampled ?? target,
|
|
12944
|
+
resolveTarget: multisampled !== null ? target : undefined,
|
|
12945
|
+
/* Loaded, not cleared: the frame so far, with its skin added. */
|
|
12946
|
+
loadOp: 'load',
|
|
12947
|
+
storeOp: resolvedStoreOp(
|
|
12948
|
+
multisampled !== null,
|
|
12949
|
+
false,
|
|
12950
|
+
this.quality.discardResolvedAttachments,
|
|
12951
|
+
),
|
|
12952
|
+
},
|
|
12953
|
+
],
|
|
12954
|
+
depthStencilAttachment: { view: depth, depthLoadOp: 'load', depthStoreOp: 'store' },
|
|
12955
|
+
});
|
|
12956
|
+
}
|
|
12957
|
+
|
|
12350
12958
|
private takeVolumeDepth(): boolean {
|
|
12351
12959
|
/*
|
|
12352
12960
|
* **Never while the mirror is open, and this is a correctness fix rather than a saving.**
|
|
@@ -12969,6 +13577,8 @@ export class WebGPURenderer implements RendererApi {
|
|
|
12969
13577
|
* `beginFrame` resets it. Silent, and exactly the kind of loss the arena is designed
|
|
12970
13578
|
* against everywhere else.
|
|
12971
13579
|
*/
|
|
13580
|
+
/* Before the last flush, which may let the depth go that the spread still reads. */
|
|
13581
|
+
this.runSkinScatter();
|
|
12972
13582
|
this.flushGraph('boundary', this.liveOutFinalFlush());
|
|
12973
13583
|
/*
|
|
12974
13584
|
* `ensurePass` rather than a null check: the frame's pass is opened on demand now, so a
|
|
@@ -16141,6 +16751,7 @@ export class WebGPURenderer implements RendererApi {
|
|
|
16141
16751
|
* the 2026-08-13 rule asks for — one decision, bound twice.
|
|
16142
16752
|
*/
|
|
16143
16753
|
dispose(): void {
|
|
16754
|
+
this.skinScatter?.dispose();
|
|
16144
16755
|
this.motionPass?.dispose();
|
|
16145
16756
|
this.motionPass = null;
|
|
16146
16757
|
this.latePass?.dispose();
|
|
@@ -16181,3 +16792,12 @@ export class WebGPURenderer implements RendererApi {
|
|
|
16181
16792
|
this.surface.dispose();
|
|
16182
16793
|
}
|
|
16183
16794
|
}
|
|
16795
|
+
|
|
16796
|
+
/**
|
|
16797
|
+
* The views a binding and a character's particles bind as, built once a pair and kept on the
|
|
16798
|
+
* particles: a bind group is cached by its views' identity, so a fresh object a draw would rebuild
|
|
16799
|
+
* the group every draw.
|
|
16800
|
+
*/
|
|
16801
|
+
function clothViewsOf(binding: GpuClothBinding, particles: GpuClothParticles): ClothViews {
|
|
16802
|
+
return particles.viewsWith(binding);
|
|
16803
|
+
}
|