reze-engine 0.11.1 → 0.11.3

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.
Files changed (110) hide show
  1. package/dist/engine.d.ts +5 -3
  2. package/dist/engine.d.ts.map +1 -1
  3. package/dist/engine.js +72 -425
  4. package/dist/index.d.ts +1 -2
  5. package/dist/index.d.ts.map +1 -1
  6. package/dist/shaders/body.d.ts +1 -1
  7. package/dist/shaders/body.d.ts.map +1 -1
  8. package/dist/shaders/body.js +27 -60
  9. package/dist/shaders/cloth_rough.d.ts +1 -1
  10. package/dist/shaders/cloth_rough.d.ts.map +1 -1
  11. package/dist/shaders/cloth_rough.js +4 -16
  12. package/dist/shaders/cloth_smooth.d.ts +1 -1
  13. package/dist/shaders/cloth_smooth.d.ts.map +1 -1
  14. package/dist/shaders/cloth_smooth.js +5 -17
  15. package/dist/shaders/default.d.ts +1 -1
  16. package/dist/shaders/default.d.ts.map +1 -1
  17. package/dist/shaders/default.js +20 -34
  18. package/dist/shaders/dfg_lut.d.ts +1 -1
  19. package/dist/shaders/dfg_lut.d.ts.map +1 -1
  20. package/dist/shaders/dfg_lut.js +1 -1
  21. package/dist/shaders/eye.d.ts +1 -1
  22. package/dist/shaders/eye.d.ts.map +1 -1
  23. package/dist/shaders/eye.js +22 -35
  24. package/dist/shaders/face.d.ts +1 -1
  25. package/dist/shaders/face.d.ts.map +1 -1
  26. package/dist/shaders/face.js +21 -57
  27. package/dist/shaders/hair.d.ts +1 -1
  28. package/dist/shaders/hair.d.ts.map +1 -1
  29. package/dist/shaders/hair.js +7 -27
  30. package/dist/shaders/materials/body.d.ts +2 -0
  31. package/dist/shaders/materials/body.d.ts.map +1 -0
  32. package/dist/shaders/materials/body.js +199 -0
  33. package/dist/shaders/materials/cloth_rough.d.ts +2 -0
  34. package/dist/shaders/materials/cloth_rough.d.ts.map +1 -0
  35. package/dist/shaders/materials/cloth_rough.js +178 -0
  36. package/dist/shaders/materials/cloth_smooth.d.ts +2 -0
  37. package/dist/shaders/materials/cloth_smooth.d.ts.map +1 -0
  38. package/dist/shaders/materials/cloth_smooth.js +174 -0
  39. package/dist/shaders/materials/default.d.ts +2 -0
  40. package/dist/shaders/materials/default.d.ts.map +1 -0
  41. package/dist/shaders/materials/default.js +171 -0
  42. package/dist/shaders/materials/eye.d.ts +2 -0
  43. package/dist/shaders/materials/eye.d.ts.map +1 -0
  44. package/dist/shaders/materials/eye.js +146 -0
  45. package/dist/shaders/materials/face.d.ts +2 -0
  46. package/dist/shaders/materials/face.d.ts.map +1 -0
  47. package/dist/shaders/materials/face.js +199 -0
  48. package/dist/shaders/materials/hair.d.ts +2 -0
  49. package/dist/shaders/materials/hair.d.ts.map +1 -0
  50. package/dist/shaders/materials/hair.js +176 -0
  51. package/dist/shaders/materials/metal.d.ts +2 -0
  52. package/dist/shaders/materials/metal.d.ts.map +1 -0
  53. package/dist/shaders/materials/metal.js +183 -0
  54. package/dist/shaders/materials/nodes.d.ts +2 -0
  55. package/dist/shaders/materials/nodes.d.ts.map +1 -0
  56. package/{src/shaders/nodes.ts → dist/shaders/materials/nodes.js} +32 -16
  57. package/dist/shaders/materials/stockings.d.ts +2 -0
  58. package/dist/shaders/materials/stockings.d.ts.map +1 -0
  59. package/dist/shaders/materials/stockings.js +244 -0
  60. package/dist/shaders/metal.d.ts +1 -1
  61. package/dist/shaders/metal.d.ts.map +1 -1
  62. package/dist/shaders/metal.js +4 -17
  63. package/dist/shaders/nodes.d.ts +1 -1
  64. package/dist/shaders/nodes.d.ts.map +1 -1
  65. package/dist/shaders/nodes.js +0 -9
  66. package/dist/shaders/passes/bloom.d.ts +4 -0
  67. package/dist/shaders/passes/bloom.d.ts.map +1 -0
  68. package/dist/shaders/passes/bloom.js +117 -0
  69. package/dist/shaders/passes/composite.d.ts +2 -0
  70. package/dist/shaders/passes/composite.d.ts.map +1 -0
  71. package/dist/shaders/passes/composite.js +61 -0
  72. package/dist/shaders/passes/ground.d.ts +2 -0
  73. package/dist/shaders/passes/ground.d.ts.map +1 -0
  74. package/dist/shaders/passes/ground.js +93 -0
  75. package/dist/shaders/passes/mipmap.d.ts +2 -0
  76. package/dist/shaders/passes/mipmap.d.ts.map +1 -0
  77. package/dist/shaders/passes/mipmap.js +16 -0
  78. package/dist/shaders/passes/outline.d.ts +2 -0
  79. package/dist/shaders/passes/outline.d.ts.map +1 -0
  80. package/dist/shaders/passes/outline.js +83 -0
  81. package/dist/shaders/passes/pick.d.ts +2 -0
  82. package/dist/shaders/passes/pick.d.ts.map +1 -0
  83. package/dist/shaders/passes/pick.js +39 -0
  84. package/dist/shaders/passes/shadow.d.ts +2 -0
  85. package/dist/shaders/passes/shadow.d.ts.map +1 -0
  86. package/dist/shaders/passes/shadow.js +16 -0
  87. package/dist/shaders/stockings.d.ts +1 -1
  88. package/dist/shaders/stockings.d.ts.map +1 -1
  89. package/package.json +2 -2
  90. package/src/engine.ts +112 -449
  91. package/src/index.ts +3 -2
  92. package/src/shaders/dfg_lut.ts +1 -1
  93. package/src/shaders/{body.ts → materials/body.ts} +27 -60
  94. package/src/shaders/{cloth_rough.ts → materials/cloth_rough.ts} +4 -16
  95. package/src/shaders/{cloth_smooth.ts → materials/cloth_smooth.ts} +5 -17
  96. package/src/shaders/{default.ts → materials/default.ts} +21 -34
  97. package/src/shaders/{eye.ts → materials/eye.ts} +23 -35
  98. package/src/shaders/{face.ts → materials/face.ts} +21 -57
  99. package/src/shaders/{hair.ts → materials/hair.ts} +7 -27
  100. package/src/shaders/{metal.ts → materials/metal.ts} +15 -19
  101. package/src/shaders/materials/nodes.ts +483 -0
  102. package/src/shaders/passes/bloom.ts +121 -0
  103. package/src/shaders/passes/composite.ts +62 -0
  104. package/src/shaders/passes/ground.ts +94 -0
  105. package/src/shaders/passes/mipmap.ts +17 -0
  106. package/src/shaders/passes/outline.ts +84 -0
  107. package/src/shaders/passes/pick.ts +40 -0
  108. package/src/shaders/passes/shadow.ts +17 -0
  109. package/src/shaders/classify.ts +0 -25
  110. /package/src/shaders/{stockings.ts → materials/stockings.ts} +0 -0
package/src/engine.ts CHANGED
@@ -13,18 +13,51 @@ import {
13
13
  normalizeAssetPath,
14
14
  type AssetReader,
15
15
  } from "./asset-reader"
16
- import { DEFAULT_SHADER_WGSL } from "./shaders/default"
16
+ import { DEFAULT_SHADER_WGSL } from "./shaders/materials/default"
17
+ import { FACE_SHADER_WGSL } from "./shaders/materials/face"
18
+ import { HAIR_SHADER_WGSL } from "./shaders/materials/hair"
19
+ import { CLOTH_SMOOTH_SHADER_WGSL } from "./shaders/materials/cloth_smooth"
20
+ import { CLOTH_ROUGH_SHADER_WGSL } from "./shaders/materials/cloth_rough"
21
+ import { METAL_SHADER_WGSL } from "./shaders/materials/metal"
22
+ import { BODY_SHADER_WGSL } from "./shaders/materials/body"
23
+ import { EYE_SHADER_WGSL } from "./shaders/materials/eye"
24
+ import { STOCKINGS_SHADER_WGSL } from "./shaders/materials/stockings"
17
25
  import { BRDF_LUT_SIZE, BRDF_LUT_BAKE_WGSL } from "./shaders/dfg_lut"
18
26
  import { LTC_MAG_LUT_SIZE, LTC_MAG_LUT_DATA } from "./shaders/ltc_mag_lut"
19
- import { FACE_SHADER_WGSL } from "./shaders/face"
20
- import { HAIR_SHADER_WGSL } from "./shaders/hair"
21
- import { CLOTH_SMOOTH_SHADER_WGSL } from "./shaders/cloth_smooth"
22
- import { CLOTH_ROUGH_SHADER_WGSL } from "./shaders/cloth_rough"
23
- import { METAL_SHADER_WGSL } from "./shaders/metal"
24
- import { BODY_SHADER_WGSL } from "./shaders/body"
25
- import { EYE_SHADER_WGSL } from "./shaders/eye"
26
- import { STOCKINGS_SHADER_WGSL } from "./shaders/stockings"
27
- import { resolvePreset, type MaterialPreset, type MaterialPresetMap } from "./shaders/classify"
27
+ import { SHADOW_DEPTH_SHADER_WGSL } from "./shaders/passes/shadow"
28
+ import { GROUND_SHADOW_SHADER_WGSL } from "./shaders/passes/ground"
29
+ import { OUTLINE_SHADER_WGSL } from "./shaders/passes/outline"
30
+ import {
31
+ BLOOM_BLIT_SHADER_WGSL,
32
+ BLOOM_DOWNSAMPLE_SHADER_WGSL,
33
+ BLOOM_UPSAMPLE_SHADER_WGSL,
34
+ } from "./shaders/passes/bloom"
35
+ import { COMPOSITE_SHADER_WGSL } from "./shaders/passes/composite"
36
+ import { PICK_SHADER_WGSL } from "./shaders/passes/pick"
37
+ import { MIPMAP_BLIT_SHADER_WGSL } from "./shaders/passes/mipmap"
38
+
39
+ // Material preset dispatch. Consumers supply a MaterialPresetMap assigning material names
40
+ // to presets; unmapped materials fall back to "default" (Principled BSDF).
41
+ export type MaterialPreset =
42
+ | "default"
43
+ | "face"
44
+ | "hair"
45
+ | "body"
46
+ | "eye"
47
+ | "stockings"
48
+ | "metal"
49
+ | "cloth_smooth"
50
+ | "cloth_rough"
51
+
52
+ export type MaterialPresetMap = Partial<Record<MaterialPreset, string[]>>
53
+
54
+ function resolvePreset(materialName: string, map: MaterialPresetMap | undefined): MaterialPreset {
55
+ if (!map) return "default"
56
+ for (const [preset, names] of Object.entries(map)) {
57
+ if (names && names.includes(materialName)) return preset as MaterialPreset
58
+ }
59
+ return "default"
60
+ }
28
61
 
29
62
  export type RaycastCallback = (modelName: string, material: string | null, screenX: number, screenY: number) => void
30
63
 
@@ -84,15 +117,17 @@ export const DEFAULT_BLOOM_OPTIONS: BloomOptions = {
84
117
 
85
118
  /** Blender Color Management / View (rendering.txt: Filmic, exposure, gamma). `look` is reserved for future curve tweaks. */
86
119
  export type ViewTransformOptions = {
87
- /** Stops applied before Filmic: `linear *= 2^exposure` (Blender default often ~−0.3). */
120
+ /** Stops applied before Filmic: `linear *= 2^exposure`. */
88
121
  exposure: number
89
122
  /** After Filmic, display gamma (`pow(rgb, 1/gamma)`). */
90
123
  gamma: number
91
124
  look: "default" | "medium_high_contrast"
92
125
  }
93
126
 
127
+ // Matches the reference Blender project: Filmic view, Medium High Contrast look,
128
+ // exposure 0.3, gamma 1.0, sRGB display, no curves.
94
129
  export const DEFAULT_VIEW_TRANSFORM: ViewTransformOptions = {
95
- exposure: -0.30000001192092896,
130
+ exposure: 0.6,
96
131
  gamma: 1.0,
97
132
  look: "medium_high_contrast",
98
133
  }
@@ -221,11 +256,15 @@ export class Engine {
221
256
  private maskResolveView!: GPUTextureView
222
257
  private renderPassDescriptor!: GPURenderPassDescriptor
223
258
  private compositePassDescriptor!: GPURenderPassDescriptor
224
- private compositePipeline!: GPURenderPipeline
259
+ // Two specialized composite pipelines via WGSL pipeline-override constants.
260
+ // Identity variant skips the gamma pow entirely at shader-compile time —
261
+ // Safari's Metal backend won't fold pow(x, 1) to identity.
262
+ private compositePipelineIdentity!: GPURenderPipeline
263
+ private compositePipelineGamma!: GPURenderPipeline
225
264
  private compositeBindGroupLayout!: GPUBindGroupLayout
226
265
  private compositeBindGroup!: GPUBindGroup
227
266
  private compositeUniformBuffer!: GPUBuffer
228
- // [exposure, gamma, _, _, bloomTint.x, bloomTint.y, bloomTint.z, bloomIntensity]
267
+ // [exposure, invGamma, _, _, bloomTint.x, bloomTint.y, bloomTint.z, bloomIntensity]
229
268
  private readonly compositeUniformData = new Float32Array(8)
230
269
 
231
270
  // EEVEE-style bloom pyramid (mirrors Blender 3.6 effect_bloom_frag.glsl):
@@ -401,7 +440,10 @@ export class Engine {
401
440
  const effIntensity = b.enabled ? b.intensity : 0.0
402
441
  const u = this.compositeUniformData
403
442
  u[0] = v.exposure
404
- u[1] = Math.max(v.gamma, 1e-4)
443
+ // Store 1/gamma so the shader avoids a per-pixel divide. Safari's Metal
444
+ // compiler doesn't fold `pow(x, 1/g)` into identity when g=1, so also emit
445
+ // a uniform branch that skips the pow entirely in the common case.
446
+ u[1] = 1.0 / Math.max(v.gamma, 1e-4)
405
447
  u[2] = 0.0
406
448
  u[3] = 0.0
407
449
  u[4] = b.color.x
@@ -435,9 +477,12 @@ export class Engine {
435
477
  private writeBloomUniforms(): void {
436
478
  const b = this.bloomSettings
437
479
  const bu = this.bloomBlitUniformData
438
- // EEVEE prefilter: threshold, knee, clamp (0 → disabled), _unused
480
+ // EEVEE prefilter: threshold, knee_half, clamp (0 → disabled), _unused
481
+ // Blender halves the knee before passing to the shader (eevee_bloom.c: knee * 0.5f).
482
+ // The blit shader's quadratic soft-knee curve uses knee_half as the offset from threshold,
483
+ // so the soft ramp spans [threshold - knee/2 .. threshold + knee/2] — NOT [threshold - knee .. threshold + knee].
439
484
  bu[0] = b.threshold
440
- bu[1] = b.knee
485
+ bu[1] = b.knee * 0.5
441
486
  bu[2] = b.clamp
442
487
  bu[3] = 0.0
443
488
  this.device.queue.writeBuffer(this.bloomBlitUniformBuffer, 0, bu)
@@ -520,7 +565,7 @@ export class Engine {
520
565
  { texture: ltcTemp },
521
566
  half,
522
567
  { bytesPerRow: LTC_MAG_LUT_SIZE * 4, rowsPerImage: LTC_MAG_LUT_SIZE },
523
- { width: LTC_MAG_LUT_SIZE, height: LTC_MAG_LUT_SIZE, depthOrArrayLayers: 1 }
568
+ { width: LTC_MAG_LUT_SIZE, height: LTC_MAG_LUT_SIZE, depthOrArrayLayers: 1 },
524
569
  )
525
570
 
526
571
  this.brdfLutTexture = this.device.createTexture({
@@ -888,21 +933,7 @@ export class Engine {
888
933
  })
889
934
  const shadowShader = this.device.createShaderModule({
890
935
  label: "shadow depth",
891
- code: /* wgsl */ `
892
- struct LightVP { viewProj: mat4x4f, };
893
- @group(0) @binding(0) var<uniform> lp: LightVP;
894
- @group(0) @binding(1) var<storage, read> skinMats: array<mat4x4f>;
895
- @vertex fn vs(@location(0) position: vec3f, @location(1) normal: vec3f, @location(2) uv: vec2f,
896
- @location(3) joints0: vec4<u32>, @location(4) weights0: vec4<f32>) -> @builtin(position) vec4f {
897
- let pos4 = vec4f(position, 1.0);
898
- let ws = weights0.x + weights0.y + weights0.z + weights0.w;
899
- let inv = select(1.0, 1.0 / ws, ws > 0.0001);
900
- let nw = select(vec4f(1.0,0.0,0.0,0.0), weights0 * inv, ws > 0.0001);
901
- var sp = vec4f(0.0);
902
- for (var i = 0u; i < 4u; i++) { sp += (skinMats[joints0[i]] * pos4) * nw[i]; }
903
- return lp.viewProj * vec4f(sp.xyz, 1.0);
904
- }
905
- `,
936
+ code: SHADOW_DEPTH_SHADER_WGSL,
906
937
  })
907
938
  this.shadowDepthPipeline = this.device.createRenderPipeline({
908
939
  label: "shadow depth pipeline",
@@ -962,99 +993,7 @@ export class Engine {
962
993
  })
963
994
  const groundShadowShader = this.device.createShaderModule({
964
995
  label: "ground shadow",
965
- code: /* wgsl */ `
966
- struct CameraUniforms { view: mat4x4f, projection: mat4x4f, viewPos: vec3f, _p: f32, };
967
- struct Light { direction: vec4f, color: vec4f, };
968
- struct LightUniforms { ambientColor: vec4f, lights: array<Light, 4>, };
969
- struct GroundShadowMat {
970
- diffuseColor: vec3f, fadeStart: f32,
971
- fadeEnd: f32, shadowStrength: f32, pcfTexel: f32, gridSpacing: f32,
972
- gridLineWidth: f32, gridLineOpacity: f32, noiseStrength: f32, _pad: f32,
973
- gridLineColor: vec3f, _pad2: f32,
974
- };
975
- struct LightVP { viewProj: mat4x4f, };
976
- @group(0) @binding(0) var<uniform> camera: CameraUniforms;
977
- @group(0) @binding(1) var<uniform> light: LightUniforms;
978
- @group(0) @binding(2) var shadowMap: texture_depth_2d;
979
- @group(0) @binding(3) var shadowSampler: sampler_comparison;
980
- @group(0) @binding(4) var<uniform> material: GroundShadowMat;
981
- @group(0) @binding(5) var<uniform> lightVP: LightVP;
982
-
983
- // Hash-based noise for frosted/matte surface
984
- fn hash2(p: vec2f) -> f32 {
985
- var p3 = fract(vec3f(p.x, p.y, p.x) * 0.1031);
986
- p3 += dot(p3, vec3f(p3.y + 33.33, p3.z + 33.33, p3.x + 33.33));
987
- return fract((p3.x + p3.y) * p3.z);
988
- }
989
- fn valueNoise(p: vec2f) -> f32 {
990
- let i = floor(p);
991
- let f = fract(p);
992
- let u = f * f * (3.0 - 2.0 * f);
993
- return mix(mix(hash2(i), hash2(i + vec2f(1.0, 0.0)), u.x),
994
- mix(hash2(i + vec2f(0.0, 1.0)), hash2(i + vec2f(1.0, 1.0)), u.x), u.y);
995
- }
996
- fn fbmNoise(p: vec2f) -> f32 {
997
- var v = 0.0;
998
- var a = 0.5;
999
- var pp = p;
1000
- for (var i = 0; i < 4; i++) {
1001
- v += a * valueNoise(pp);
1002
- pp *= 2.0;
1003
- a *= 0.5;
1004
- }
1005
- return v;
1006
- }
1007
-
1008
- struct VO { @builtin(position) position: vec4f, @location(0) worldPos: vec3f, @location(1) normal: vec3f, };
1009
- @vertex fn vs(@location(0) position: vec3f, @location(1) normal: vec3f, @location(2) uv: vec2f) -> VO {
1010
- var o: VO; o.worldPos = position; o.normal = normal;
1011
- o.position = camera.projection * camera.view * vec4f(position, 1.0); return o;
1012
- }
1013
- struct FSOut { @location(0) color: vec4f, @location(1) mask: f32 };
1014
- @fragment fn fs(i: VO) -> FSOut {
1015
- let n = normalize(i.normal);
1016
- let centerDist = length(i.worldPos.xz);
1017
- let edgeFade = 1.0 - smoothstep(0.0, 1.0, clamp((centerDist - material.fadeStart) / max(material.fadeEnd - material.fadeStart, 0.001), 0.0, 1.0));
1018
-
1019
- // Shadow sampling
1020
- let lclip = lightVP.viewProj * vec4f(i.worldPos, 1.0);
1021
- let ndc = lclip.xyz / max(lclip.w, 1e-6);
1022
- let suv = vec2f(ndc.x * 0.5 + 0.5, 0.5 - ndc.y * 0.5);
1023
- let suv_c = clamp(suv, vec2f(0.02), vec2f(0.98));
1024
- let st = material.pcfTexel;
1025
- let compareZ = ndc.z - 0.0035;
1026
- var vis = 0.0;
1027
- for (var y = -2; y <= 2; y++) {
1028
- for (var x = -2; x <= 2; x++) {
1029
- vis += textureSampleCompare(shadowMap, shadowSampler, suv_c + vec2f(f32(x), f32(y)) * st, compareZ);
1030
- }
1031
- }
1032
- vis *= 0.04;
1033
-
1034
- // Frosted/matte micro-texture (磨砂)
1035
- let noiseVal = fbmNoise(i.worldPos.xz * 3.0);
1036
- let noiseTint = 1.0 + (noiseVal - 0.5) * material.noiseStrength;
1037
-
1038
- // Grid lines — anti-aliased via screen-space derivatives
1039
- let gp = i.worldPos.xz / material.gridSpacing;
1040
- let gridFrac = abs(fract(gp - 0.5) - 0.5);
1041
- let gridDeriv = fwidth(gp);
1042
- let halfLine = material.gridLineWidth * 0.5;
1043
- let gridLine = 1.0 - min(
1044
- smoothstep(halfLine - gridDeriv.x, halfLine + gridDeriv.x, gridFrac.x),
1045
- smoothstep(halfLine - gridDeriv.y, halfLine + gridDeriv.y, gridFrac.y)
1046
- );
1047
- let sun = light.ambientColor.xyz + light.lights[0].color.xyz * light.lights[0].color.w * max(dot(n, -light.lights[0].direction.xyz), 0.0);
1048
- let dark = (1.0 - vis) * material.shadowStrength;
1049
- var baseColor = material.diffuseColor * sun * (1.0 - dark * 0.65);
1050
- baseColor *= noiseTint;
1051
- let finalColor = mix(baseColor, material.gridLineColor, gridLine * material.gridLineOpacity * edgeFade);
1052
- var out: FSOut;
1053
- out.color = vec4f(finalColor * edgeFade, edgeFade);
1054
- out.mask = 0.0;
1055
- return out;
1056
- }
1057
- `,
996
+ code: GROUND_SHADOW_SHADER_WGSL,
1058
997
  })
1059
998
  this.groundShadowPipeline = this.createRenderPipeline({
1060
999
  label: "ground shadow pipeline",
@@ -1098,90 +1037,7 @@ export class Engine {
1098
1037
 
1099
1038
  const outlineShaderModule = this.device.createShaderModule({
1100
1039
  label: "outline shaders",
1101
- code: /* wgsl */ `
1102
- struct CameraUniforms {
1103
- view: mat4x4f,
1104
- projection: mat4x4f,
1105
- viewPos: vec3f,
1106
- _padding: f32,
1107
- };
1108
-
1109
- struct MaterialUniforms {
1110
- edgeColor: vec4f,
1111
- edgeSize: f32,
1112
- _padding1: f32,
1113
- _padding2: f32,
1114
- _padding3: f32,
1115
- };
1116
-
1117
- // group 0: per-frame
1118
- @group(0) @binding(0) var<uniform> camera: CameraUniforms;
1119
- // group 1: per-instance
1120
- @group(1) @binding(0) var<storage, read> skinMats: array<mat4x4f>;
1121
- // group 2: per-material
1122
- @group(2) @binding(0) var<uniform> material: MaterialUniforms;
1123
-
1124
- struct VertexOutput {
1125
- @builtin(position) position: vec4f,
1126
- };
1127
-
1128
- @vertex fn vs(
1129
- @location(0) position: vec3f,
1130
- @location(1) normal: vec3f,
1131
- @location(3) joints0: vec4<u32>,
1132
- @location(4) weights0: vec4<f32>
1133
- ) -> VertexOutput {
1134
- var output: VertexOutput;
1135
- let pos4 = vec4f(position, 1.0);
1136
-
1137
- let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;
1138
- let invWeightSum = select(1.0, 1.0 / weightSum, weightSum > 0.0001);
1139
- let normalizedWeights = select(vec4f(1.0, 0.0, 0.0, 0.0), weights0 * invWeightSum, weightSum > 0.0001);
1140
-
1141
- var skinnedPos = vec4f(0.0, 0.0, 0.0, 0.0);
1142
- var skinnedNrm = vec3f(0.0, 0.0, 0.0);
1143
- for (var i = 0u; i < 4u; i++) {
1144
- let j = joints0[i];
1145
- let w = normalizedWeights[i];
1146
- let m = skinMats[j];
1147
- skinnedPos += (m * pos4) * w;
1148
- let r3 = mat3x3f(m[0].xyz, m[1].xyz, m[2].xyz);
1149
- skinnedNrm += (r3 * normal) * w;
1150
- }
1151
- let worldPos = skinnedPos.xyz;
1152
- let worldNormal = normalize(skinnedNrm);
1153
-
1154
- // Screen-space outline extrusion — MMD-style pixel-stable edge line.
1155
- // 1. Project position and normal-as-direction to clip space.
1156
- // 2. Normalize the 2D clip-space normal, aspect-compensated so "one pixel horizontally"
1157
- // matches "one pixel vertically" (otherwise wide viewports squash the outline in X).
1158
- // 3. Offset clip-space xy by (normal * edgeSize * edgeScale), then multiply by w
1159
- // so the perspective divide cancels out → offset stays constant in NDC regardless
1160
- // of depth, matching how MMD / babylon-mmd style outlines look identical when zooming.
1161
- // 4. edgeScale is in NDC-y units per PMX edgeSize. ≈ 0.006 gives ~3px at 1080p; it's
1162
- // tied to viewport HEIGHT so resizing the window keeps pixel thickness stable.
1163
- let viewProj = camera.projection * camera.view;
1164
- let clipPos = viewProj * vec4f(worldPos, 1.0);
1165
- let clipNormal = (viewProj * vec4f(worldNormal, 0.0)).xy;
1166
- // projection is column-major: proj[0][0] = 1/(aspect·tan(fov/2)), proj[1][1] = 1/tan(fov/2).
1167
- // Ratio proj[1][1]/proj[0][0] recovers the viewport aspect (width/height).
1168
- let aspect = camera.projection[1][1] / camera.projection[0][0];
1169
- let pixelDir = normalize(vec2f(clipNormal.x * aspect, clipNormal.y));
1170
- let ndcDir = vec2f(pixelDir.x / aspect, pixelDir.y);
1171
- let edgeScale = 0.0016;
1172
- let offset = ndcDir * material.edgeSize * edgeScale * clipPos.w;
1173
- output.position = vec4f(clipPos.xy + offset, clipPos.z, clipPos.w);
1174
- return output;
1175
- }
1176
-
1177
- struct FSOut { @location(0) color: vec4f, @location(1) mask: f32 };
1178
- @fragment fn fs() -> FSOut {
1179
- var out: FSOut;
1180
- out.color = material.edgeColor;
1181
- out.mask = 1.0;
1182
- return out;
1183
- }
1184
- `,
1040
+ code: OUTLINE_SHADER_WGSL,
1185
1041
  })
1186
1042
 
1187
1043
  this.outlinePipeline = this.createRenderPipeline({
@@ -1246,134 +1102,25 @@ export class Engine {
1246
1102
  ],
1247
1103
  })
1248
1104
 
1249
- const bloomFullscreenVs = /* wgsl */ `
1250
- @vertex fn vs(@builtin(vertex_index) vi: u32) -> @builtin(position) vec4f {
1251
- let x = f32((vi & 1u) << 2u) - 1.0;
1252
- let y = f32((vi & 2u) << 1u) - 1.0;
1253
- return vec4f(x, y, 0.0, 1.0);
1254
- }
1255
- `
1256
-
1257
- // Blit: full-res HDR → half-res. Karis 4-tap firefly average + EEVEE quadratic knee threshold + clamp.
1258
1105
  const bloomBlitShader = this.device.createShaderModule({
1259
1106
  label: "bloom blit (Karis prefilter)",
1260
- code: `${bloomFullscreenVs}
1261
- @group(0) @binding(0) var hdrTex: texture_2d<f32>;
1262
- @group(0) @binding(1) var<uniform> prefilter: vec4<f32>; // threshold, knee, clamp, _unused
1263
- @group(0) @binding(2) var maskTex: texture_2d<f32>;
1264
-
1265
- fn luminance(c: vec3f) -> f32 {
1266
- return dot(max(c, vec3f(0.0)), vec3f(0.2126, 0.7152, 0.0722));
1267
- }
1268
- fn fetch(c: vec2<i32>, clampV: f32) -> vec3f {
1269
- let d = vec2<i32>(textureDimensions(hdrTex));
1270
- let cc = clamp(c, vec2<i32>(0), d - vec2<i32>(1));
1271
- let s = textureLoad(hdrTex, cc, 0);
1272
- // Scene pass uses src-alpha blend with clear alpha 0 → premultiplied. Unpremultiply.
1273
- let rgb = max(s.rgb / max(s.a, 1e-6), vec3f(0.0));
1274
- // Bloom mask: MRT r8unorm written by material shaders (1.0 = bloom, 0.0 = skip).
1275
- let mask = textureLoad(maskTex, cc, 0).r;
1276
- let masked = rgb * mask;
1277
- // Blender: clamp each tap BEFORE Karis average (eevee_bloom: color = min(clampIntensity, color)).
1278
- return select(masked, min(masked, vec3f(clampV)), clampV > 0.0);
1279
- }
1280
-
1281
- @fragment fn fs(@builtin(position) p: vec4f) -> @location(0) vec4f {
1282
- let dst = vec2<i32>(p.xy - vec2f(0.5));
1283
- let base = dst * 2;
1284
- let clampV = prefilter.z;
1285
- let a = fetch(base + vec2<i32>(0, 0), clampV);
1286
- let b = fetch(base + vec2<i32>(1, 0), clampV);
1287
- let c = fetch(base + vec2<i32>(0, 1), clampV);
1288
- let d = fetch(base + vec2<i32>(1, 1), clampV);
1289
- // Karis partial average: weight each tap by 1/(1+luma) — suppresses fireflies.
1290
- let wa = 1.0 / (1.0 + luminance(a));
1291
- let wb = 1.0 / (1.0 + luminance(b));
1292
- let wc = 1.0 / (1.0 + luminance(c));
1293
- let wd = 1.0 / (1.0 + luminance(d));
1294
- let avg = (a * wa + b * wb + c * wc + d * wd) / max(wa + wb + wc + wd, 1e-6);
1295
- // EEVEE quadratic threshold (brightness = max-channel, then soft-knee curve).
1296
- let bright = max(avg.r, max(avg.g, avg.b));
1297
- let soft = clamp(bright - prefilter.x + prefilter.y, 0.0, 2.0 * prefilter.y);
1298
- let q = (soft * soft) / (4.0 * max(prefilter.y, 1e-4) + 1e-6);
1299
- let contrib = max(q, bright - prefilter.x) / max(bright, 1e-4);
1300
- return vec4f(max(avg * contrib, vec3f(0.0)), 1.0);
1301
- }
1302
- `,
1107
+ code: BLOOM_BLIT_SHADER_WGSL,
1303
1108
  })
1304
1109
 
1305
- // Downsample: Jimenez/COD 13-tap dual-box — 5 weighted 2×2 averages, rejects nyquist ringing.
1306
1110
  const bloomDownsampleShader = this.device.createShaderModule({
1307
1111
  label: "bloom downsample 13-tap",
1308
- code: `${bloomFullscreenVs}
1309
- @group(0) @binding(0) var srcTex: texture_2d<f32>;
1310
- @group(0) @binding(1) var srcSamp: sampler;
1311
-
1312
- fn samp(uv: vec2f, off: vec2f) -> vec3f {
1313
- return textureSampleLevel(srcTex, srcSamp, uv + off, 0.0).rgb;
1314
- }
1315
-
1316
- @fragment fn fs(@builtin(position) p: vec4f) -> @location(0) vec4f {
1317
- let srcDims = vec2f(textureDimensions(srcTex));
1318
- let t = 1.0 / srcDims;
1319
- // fragCoord.xy reports pixel centers (e.g. 0.5,0.5 for first pixel) — divide by dst dims directly.
1320
- let dstDims = srcDims * 0.5;
1321
- let uv = p.xy / max(dstDims, vec2f(1.0));
1322
- let A = samp(uv, t * vec2f(-2.0, -2.0));
1323
- let B = samp(uv, t * vec2f( 0.0, -2.0));
1324
- let C = samp(uv, t * vec2f( 2.0, -2.0));
1325
- let D = samp(uv, t * vec2f(-1.0, -1.0));
1326
- let E = samp(uv, t * vec2f( 1.0, -1.0));
1327
- let F = samp(uv, t * vec2f(-2.0, 0.0));
1328
- let G = samp(uv, t * vec2f( 0.0, 0.0));
1329
- let H = samp(uv, t * vec2f( 2.0, 0.0));
1330
- let I = samp(uv, t * vec2f(-1.0, 1.0));
1331
- let J = samp(uv, t * vec2f( 1.0, 1.0));
1332
- let K = samp(uv, t * vec2f(-2.0, 2.0));
1333
- let L = samp(uv, t * vec2f( 0.0, 2.0));
1334
- let M = samp(uv, t * vec2f( 2.0, 2.0));
1335
- var o = (D + E + I + J) * (0.5 / 4.0);
1336
- o = o + (A + B + G + F) * (0.125 / 4.0);
1337
- o = o + (B + C + H + G) * (0.125 / 4.0);
1338
- o = o + (F + G + L + K) * (0.125 / 4.0);
1339
- o = o + (G + H + M + L) * (0.125 / 4.0);
1340
- return vec4f(o, 1.0);
1341
- }
1342
- `,
1112
+ code: BLOOM_DOWNSAMPLE_SHADER_WGSL,
1343
1113
  })
1344
1114
 
1345
- // Upsample: 9-tap tent, progressively added to matching downsample mip. Blender radius = sample scale.
1346
1115
  const bloomUpsampleShader = this.device.createShaderModule({
1347
1116
  label: "bloom upsample 9-tap tent",
1348
- code: `${bloomFullscreenVs}
1349
- @group(0) @binding(0) var srcTex: texture_2d<f32>; // coarser accumulator
1350
- @group(0) @binding(1) var baseTex: texture_2d<f32>; // matching downsample mip
1351
- @group(0) @binding(2) var srcSamp: sampler;
1352
- @group(0) @binding(3) var<uniform> upU: vec4<f32>; // sampleScale, _, _, _
1353
-
1354
- @fragment fn fs(@builtin(position) p: vec4f) -> @location(0) vec4f {
1355
- let srcDims = vec2f(textureDimensions(srcTex));
1356
- let baseDims = vec2f(textureDimensions(baseTex));
1357
- let uv = p.xy / max(baseDims, vec2f(1.0));
1358
- let t = upU.x / srcDims;
1359
- var o = textureSampleLevel(srcTex, srcSamp, uv + t * vec2f(-1.0, -1.0), 0.0).rgb * 1.0;
1360
- o = o + textureSampleLevel(srcTex, srcSamp, uv + t * vec2f( 0.0, -1.0), 0.0).rgb * 2.0;
1361
- o = o + textureSampleLevel(srcTex, srcSamp, uv + t * vec2f( 1.0, -1.0), 0.0).rgb * 1.0;
1362
- o = o + textureSampleLevel(srcTex, srcSamp, uv + t * vec2f(-1.0, 0.0), 0.0).rgb * 2.0;
1363
- o = o + textureSampleLevel(srcTex, srcSamp, uv + t * vec2f( 0.0, 0.0), 0.0).rgb * 4.0;
1364
- o = o + textureSampleLevel(srcTex, srcSamp, uv + t * vec2f( 1.0, 0.0), 0.0).rgb * 2.0;
1365
- o = o + textureSampleLevel(srcTex, srcSamp, uv + t * vec2f(-1.0, 1.0), 0.0).rgb * 1.0;
1366
- o = o + textureSampleLevel(srcTex, srcSamp, uv + t * vec2f( 0.0, 1.0), 0.0).rgb * 2.0;
1367
- o = o + textureSampleLevel(srcTex, srcSamp, uv + t * vec2f( 1.0, 1.0), 0.0).rgb * 1.0;
1368
- o = o * (1.0 / 16.0);
1369
- let base = textureSampleLevel(baseTex, srcSamp, uv, 0.0).rgb;
1370
- return vec4f(o + base, 1.0);
1371
- }
1372
- `,
1117
+ code: BLOOM_UPSAMPLE_SHADER_WGSL,
1373
1118
  })
1374
1119
 
1375
1120
  const bloomBlitLayout = this.device.createPipelineLayout({ bindGroupLayouts: [this.bloomBlitBindGroupLayout] })
1376
- const bloomDownLayout = this.device.createPipelineLayout({ bindGroupLayouts: [this.bloomDownsampleBindGroupLayout] })
1121
+ const bloomDownLayout = this.device.createPipelineLayout({
1122
+ bindGroupLayouts: [this.bloomDownsampleBindGroupLayout],
1123
+ })
1377
1124
  const bloomUpLayout = this.device.createPipelineLayout({ bindGroupLayouts: [this.bloomUpsampleBindGroupLayout] })
1378
1125
 
1379
1126
  this.bloomBlitPipeline = this.device.createRenderPipeline({
@@ -1418,62 +1165,27 @@ export class Engine {
1418
1165
 
1419
1166
  const compositeShader = this.device.createShaderModule({
1420
1167
  label: "composite shader",
1421
- code: /* wgsl */ `
1422
- @group(0) @binding(0) var hdrTex: texture_2d<f32>;
1423
- @group(0) @binding(1) var bloomTex: texture_2d<f32>; // bloomUpTexture mip 0 (full pyramid top)
1424
- @group(0) @binding(2) var bloomSamp: sampler;
1425
- @group(0) @binding(3) var<uniform> viewU: array<vec4<f32>, 2>;
1426
- // viewU[0] = (exposure, gamma, _, _); viewU[1] = (tint.rgb, intensity)
1427
-
1428
- fn filmic(x: f32) -> f32 {
1429
- var lut = array<f32, 14>(
1430
- 0.0067, 0.0141, 0.0272, 0.0499, 0.0885, 0.1512, 0.2462,
1431
- 0.3753, 0.5273, 0.6776, 0.8031, 0.8929, 0.9495, 0.9814
1432
- );
1433
- let t = clamp(log2(max(x, 1e-10)) + 10.0, 0.0, 13.0);
1434
- let i = u32(t);
1435
- let j = min(i + 1u, 13u);
1436
- return mix(lut[i], lut[j], t - f32(i));
1437
- }
1438
-
1439
- @vertex fn vs(@builtin(vertex_index) vi: u32) -> @builtin(position) vec4f {
1440
- let x = f32((vi & 1u) << 2u) - 1.0;
1441
- let y = f32((vi & 2u) << 1u) - 1.0;
1442
- return vec4f(x, y, 0.0, 1.0);
1443
- }
1444
-
1445
- @fragment fn fs(@builtin(position) fragCoord: vec4f) -> @location(0) vec4f {
1446
- let hdr = textureLoad(hdrTex, vec2<i32>(fragCoord.xy), 0);
1447
- let a = max(hdr.a, 1e-6);
1448
- let straight = hdr.rgb / a;
1449
- let fullSz = vec2f(textureDimensions(hdrTex));
1450
- let bloomSz = vec2f(textureDimensions(bloomTex));
1451
- // Bloom is at half-res (pyramid mip 0). Sampler interpolates back to full-res UVs.
1452
- let bloomUv = (fragCoord.xy + vec2f(0.5)) / max(fullSz, vec2f(1.0));
1453
- let tint = viewU[1].xyz;
1454
- let intensity = viewU[1].w;
1455
- let bloom = textureSampleLevel(bloomTex, bloomSamp, bloomUv, 0.0).rgb * tint * intensity;
1456
- let combined = straight + bloom;
1457
- let exposed = combined * exp2(viewU[0].x);
1458
- let tm = vec3f(filmic(exposed.r), filmic(exposed.g), filmic(exposed.b));
1459
- let g = max(viewU[0].y, 1e-4);
1460
- let disp = pow(max(tm, vec3f(0.0)), vec3f(1.0 / g));
1461
- return vec4f(disp * hdr.a, hdr.a);
1462
- }
1463
- `,
1464
- })
1465
-
1466
- this.compositePipeline = this.device.createRenderPipeline({
1467
- label: "composite pipeline",
1468
- layout: this.device.createPipelineLayout({ bindGroupLayouts: [this.compositeBindGroupLayout] }),
1469
- vertex: { module: compositeShader, entryPoint: "vs" },
1470
- fragment: {
1471
- module: compositeShader,
1472
- entryPoint: "fs",
1473
- targets: [{ format: this.presentationFormat }],
1474
- },
1475
- primitive: { topology: "triangle-list" },
1476
- })
1168
+ code: COMPOSITE_SHADER_WGSL,
1169
+ })
1170
+
1171
+ const compositePipelineLayout = this.device.createPipelineLayout({
1172
+ bindGroupLayouts: [this.compositeBindGroupLayout],
1173
+ })
1174
+ const makeCompositePipeline = (applyGamma: boolean, label: string): GPURenderPipeline =>
1175
+ this.device.createRenderPipeline({
1176
+ label,
1177
+ layout: compositePipelineLayout,
1178
+ vertex: { module: compositeShader, entryPoint: "vs" },
1179
+ fragment: {
1180
+ module: compositeShader,
1181
+ entryPoint: "fs",
1182
+ constants: { APPLY_GAMMA: applyGamma ? 1 : 0 },
1183
+ targets: [{ format: this.presentationFormat }],
1184
+ },
1185
+ primitive: { topology: "triangle-list" },
1186
+ })
1187
+ this.compositePipelineIdentity = makeCompositePipeline(false, "composite pipeline (gamma=1)")
1188
+ this.compositePipelineGamma = makeCompositePipeline(true, "composite pipeline (gamma!=1)")
1477
1189
 
1478
1190
  this.bloomPassDescriptor = {
1479
1191
  label: "bloom pass",
@@ -1487,47 +1199,9 @@ export class Engine {
1487
1199
  ],
1488
1200
  } as GPURenderPassDescriptor
1489
1201
 
1490
- // GPU picking: encode (modelIndex, materialIndex) as color
1491
1202
  const pickShaderModule = this.device.createShaderModule({
1492
1203
  label: "pick shader",
1493
- code: /* wgsl */ `
1494
- struct CameraUniforms {
1495
- view: mat4x4f,
1496
- projection: mat4x4f,
1497
- viewPos: vec3f,
1498
- _padding: f32,
1499
- };
1500
- struct PickId {
1501
- modelId: f32,
1502
- materialId: f32,
1503
- _p1: f32,
1504
- _p2: f32,
1505
- };
1506
-
1507
- @group(0) @binding(0) var<uniform> camera: CameraUniforms;
1508
- @group(1) @binding(0) var<storage, read> skinMats: array<mat4x4f>;
1509
- @group(2) @binding(0) var<uniform> pickId: PickId;
1510
-
1511
- @vertex fn vs(
1512
- @location(0) position: vec3f,
1513
- @location(1) normal: vec3f,
1514
- @location(2) uv: vec2f,
1515
- @location(3) joints0: vec4<u32>,
1516
- @location(4) weights0: vec4<f32>
1517
- ) -> @builtin(position) vec4f {
1518
- let pos4 = vec4f(position, 1.0);
1519
- let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;
1520
- let invWeightSum = select(1.0, 1.0 / weightSum, weightSum > 0.0001);
1521
- let nw = select(vec4f(1.0, 0.0, 0.0, 0.0), weights0 * invWeightSum, weightSum > 0.0001);
1522
- var sp = vec4f(0.0);
1523
- for (var i = 0u; i < 4u; i++) { sp += (skinMats[joints0[i]] * pos4) * nw[i]; }
1524
- return camera.projection * camera.view * vec4f(sp.xyz, 1.0);
1525
- }
1526
-
1527
- @fragment fn fs() -> @location(0) vec4f {
1528
- return vec4f(pickId.modelId / 255.0, pickId.materialId / 255.0, 0.0, 1.0);
1529
- }
1530
- `,
1204
+ code: PICK_SHADER_WGSL,
1531
1205
  })
1532
1206
 
1533
1207
  this.pickPerFrameBindGroupLayout = this.device.createBindGroupLayout({
@@ -1643,10 +1317,7 @@ export class Engine {
1643
1317
  const bw = Math.max(1, Math.floor(width / 2))
1644
1318
  const bh = Math.max(1, Math.floor(height / 2))
1645
1319
  const shortSide = Math.max(1, Math.min(bw, bh))
1646
- this.bloomMipCount = Math.max(
1647
- 1,
1648
- Math.min(Engine.BLOOM_MAX_LEVELS, Math.floor(Math.log2(shortSide)) - 1),
1649
- )
1320
+ this.bloomMipCount = Math.max(1, Math.min(Engine.BLOOM_MAX_LEVELS, Math.floor(Math.log2(shortSide)) - 1))
1650
1321
  this.bloomDownTexture = this.device.createTexture({
1651
1322
  label: "bloom down pyramid",
1652
1323
  size: [bw, bh],
@@ -1663,16 +1334,12 @@ export class Engine {
1663
1334
  })
1664
1335
  this.bloomDownMipViews = []
1665
1336
  for (let i = 0; i < this.bloomMipCount; i++) {
1666
- this.bloomDownMipViews.push(
1667
- this.bloomDownTexture.createView({ baseMipLevel: i, mipLevelCount: 1 }),
1668
- )
1337
+ this.bloomDownMipViews.push(this.bloomDownTexture.createView({ baseMipLevel: i, mipLevelCount: 1 }))
1669
1338
  }
1670
1339
  this.bloomUpMipViews = []
1671
1340
  const upLevels = Math.max(1, this.bloomMipCount - 1)
1672
1341
  for (let i = 0; i < upLevels; i++) {
1673
- this.bloomUpMipViews.push(
1674
- this.bloomUpTexture.createView({ baseMipLevel: i, mipLevelCount: 1 }),
1675
- )
1342
+ this.bloomUpMipViews.push(this.bloomUpTexture.createView({ baseMipLevel: i, mipLevelCount: 1 }))
1676
1343
  }
1677
1344
 
1678
1345
  this.depthTexture = this.device.createTexture({
@@ -1685,12 +1352,16 @@ export class Engine {
1685
1352
 
1686
1353
  const depthTextureView = this.depthTexture.createView()
1687
1354
 
1355
+ // storeOp="discard" on MSAA views keeps per-sample data in Apple TBDR tile memory —
1356
+ // only the resolveTarget (hdrResolveTexture / maskResolveView) gets written to RAM.
1357
+ // With storeOp="store" Safari's Metal backend spills the full MS buffer every frame
1358
+ // (rgba16f × 4 samples on a 4K canvas ≈ 256 MB/frame of dead bandwidth).
1688
1359
  const colorAttachment: GPURenderPassColorAttachment = {
1689
1360
  view: this.multisampleTexture.createView(),
1690
1361
  resolveTarget: this.hdrResolveTexture.createView(),
1691
1362
  clearValue: { r: 0, g: 0, b: 0, a: 0 },
1692
1363
  loadOp: "clear",
1693
- storeOp: "store",
1364
+ storeOp: "discard",
1694
1365
  }
1695
1366
 
1696
1367
  const maskAttachment: GPURenderPassColorAttachment = {
@@ -1698,7 +1369,7 @@ export class Engine {
1698
1369
  resolveTarget: this.maskResolveView,
1699
1370
  clearValue: { r: 0, g: 0, b: 0, a: 0 },
1700
1371
  loadOp: "clear",
1701
- storeOp: "store",
1372
+ storeOp: "discard",
1702
1373
  }
1703
1374
 
1704
1375
  this.renderPassDescriptor = {
@@ -1708,7 +1379,8 @@ export class Engine {
1708
1379
  view: depthTextureView,
1709
1380
  depthClearValue: 1.0,
1710
1381
  depthLoadOp: "clear",
1711
- depthStoreOp: "store",
1382
+ // Main-pass depth is not sampled later (shadow uses its own map, composite is depthless).
1383
+ depthStoreOp: "discard",
1712
1384
  stencilClearValue: 0,
1713
1385
  stencilLoadOp: "clear",
1714
1386
  stencilStoreOp: "discard",
@@ -2629,20 +2301,7 @@ export class Engine {
2629
2301
  })
2630
2302
  const module = this.device.createShaderModule({
2631
2303
  label: "mipmap blit",
2632
- code: /* wgsl */ `
2633
- @group(0) @binding(0) var src: texture_2d<f32>;
2634
- @group(0) @binding(1) var samp: sampler;
2635
- @vertex fn vs(@builtin(vertex_index) vi: u32) -> @builtin(position) vec4f {
2636
- let x = f32((vi & 1u) << 2u) - 1.0;
2637
- let y = f32((vi & 2u) << 1u) - 1.0;
2638
- return vec4f(x, y, 0.0, 1.0);
2639
- }
2640
- @fragment fn fs(@builtin(position) p: vec4f) -> @location(0) vec4f {
2641
- let dstDims = vec2f(textureDimensions(src)) * 0.5;
2642
- let uv = p.xy / max(dstDims, vec2f(1.0));
2643
- return textureSampleLevel(src, samp, uv, 0.0);
2644
- }
2645
- `,
2304
+ code: MIPMAP_BLIT_SHADER_WGSL,
2646
2305
  })
2647
2306
  this.mipBlitPipeline = this.device.createRenderPipeline({
2648
2307
  label: "mipmap blit pipeline",
@@ -2665,7 +2324,9 @@ export class Engine {
2665
2324
  ],
2666
2325
  })
2667
2326
  const pass = encoder.beginRenderPass({
2668
- colorAttachments: [{ view: dstView, clearValue: { r: 0, g: 0, b: 0, a: 0 }, loadOp: "clear", storeOp: "store" }],
2327
+ colorAttachments: [
2328
+ { view: dstView, clearValue: { r: 0, g: 0, b: 0, a: 0 }, loadOp: "clear", storeOp: "store" },
2329
+ ],
2669
2330
  })
2670
2331
  pass.setPipeline(this.mipBlitPipeline)
2671
2332
  pass.setBindGroup(0, bindGroup)
@@ -2911,7 +2572,9 @@ export class Engine {
2911
2572
  const compositeAttachment = (this.compositePassDescriptor.colorAttachments as GPURenderPassColorAttachment[])[0]
2912
2573
  compositeAttachment.view = this.context.getCurrentTexture().createView()
2913
2574
  const cpass = encoder.beginRenderPass(this.compositePassDescriptor)
2914
- cpass.setPipeline(this.compositePipeline)
2575
+ const compositePipeline =
2576
+ this.viewTransform.gamma === 1.0 ? this.compositePipelineIdentity : this.compositePipelineGamma
2577
+ cpass.setPipeline(compositePipeline)
2915
2578
  cpass.setBindGroup(0, this.compositeBindGroup)
2916
2579
  cpass.draw(3)
2917
2580
  cpass.end()