reze-engine 0.11.2 → 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 (104) hide show
  1. package/dist/engine.d.ts +4 -2
  2. package/dist/engine.d.ts.map +1 -1
  3. package/dist/engine.js +58 -426
  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 +7 -28
  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/eye.d.ts +1 -1
  18. package/dist/shaders/eye.d.ts.map +1 -1
  19. package/dist/shaders/face.d.ts +1 -1
  20. package/dist/shaders/face.d.ts.map +1 -1
  21. package/dist/shaders/face.js +21 -57
  22. package/dist/shaders/hair.d.ts +1 -1
  23. package/dist/shaders/hair.d.ts.map +1 -1
  24. package/dist/shaders/hair.js +7 -27
  25. package/dist/shaders/materials/body.d.ts +2 -0
  26. package/dist/shaders/materials/body.d.ts.map +1 -0
  27. package/dist/shaders/materials/body.js +199 -0
  28. package/dist/shaders/materials/cloth_rough.d.ts +2 -0
  29. package/dist/shaders/materials/cloth_rough.d.ts.map +1 -0
  30. package/dist/shaders/materials/cloth_rough.js +178 -0
  31. package/dist/shaders/materials/cloth_smooth.d.ts +2 -0
  32. package/dist/shaders/materials/cloth_smooth.d.ts.map +1 -0
  33. package/dist/shaders/materials/cloth_smooth.js +174 -0
  34. package/dist/shaders/materials/default.d.ts +2 -0
  35. package/dist/shaders/materials/default.d.ts.map +1 -0
  36. package/dist/shaders/materials/default.js +171 -0
  37. package/dist/shaders/materials/eye.d.ts +2 -0
  38. package/dist/shaders/materials/eye.d.ts.map +1 -0
  39. package/dist/shaders/materials/eye.js +146 -0
  40. package/dist/shaders/materials/face.d.ts +2 -0
  41. package/dist/shaders/materials/face.d.ts.map +1 -0
  42. package/dist/shaders/materials/face.js +199 -0
  43. package/dist/shaders/materials/hair.d.ts +2 -0
  44. package/dist/shaders/materials/hair.d.ts.map +1 -0
  45. package/dist/shaders/materials/hair.js +176 -0
  46. package/dist/shaders/materials/metal.d.ts +2 -0
  47. package/dist/shaders/materials/metal.d.ts.map +1 -0
  48. package/dist/shaders/materials/metal.js +183 -0
  49. package/dist/shaders/materials/nodes.d.ts +2 -0
  50. package/dist/shaders/materials/nodes.d.ts.map +1 -0
  51. package/{src/shaders/nodes.ts → dist/shaders/materials/nodes.js} +32 -16
  52. package/dist/shaders/materials/stockings.d.ts +2 -0
  53. package/dist/shaders/materials/stockings.d.ts.map +1 -0
  54. package/dist/shaders/materials/stockings.js +244 -0
  55. package/dist/shaders/metal.d.ts +1 -1
  56. package/dist/shaders/metal.d.ts.map +1 -1
  57. package/dist/shaders/metal.js +4 -17
  58. package/dist/shaders/nodes.d.ts +1 -1
  59. package/dist/shaders/nodes.d.ts.map +1 -1
  60. package/dist/shaders/nodes.js +0 -9
  61. package/dist/shaders/passes/bloom.d.ts +4 -0
  62. package/dist/shaders/passes/bloom.d.ts.map +1 -0
  63. package/dist/shaders/passes/bloom.js +117 -0
  64. package/dist/shaders/passes/composite.d.ts +2 -0
  65. package/dist/shaders/passes/composite.d.ts.map +1 -0
  66. package/dist/shaders/passes/composite.js +61 -0
  67. package/dist/shaders/passes/ground.d.ts +2 -0
  68. package/dist/shaders/passes/ground.d.ts.map +1 -0
  69. package/dist/shaders/passes/ground.js +93 -0
  70. package/dist/shaders/passes/mipmap.d.ts +2 -0
  71. package/dist/shaders/passes/mipmap.d.ts.map +1 -0
  72. package/dist/shaders/passes/mipmap.js +16 -0
  73. package/dist/shaders/passes/outline.d.ts +2 -0
  74. package/dist/shaders/passes/outline.d.ts.map +1 -0
  75. package/dist/shaders/passes/outline.js +83 -0
  76. package/dist/shaders/passes/pick.d.ts +2 -0
  77. package/dist/shaders/passes/pick.d.ts.map +1 -0
  78. package/dist/shaders/passes/pick.js +39 -0
  79. package/dist/shaders/passes/shadow.d.ts +2 -0
  80. package/dist/shaders/passes/shadow.d.ts.map +1 -0
  81. package/dist/shaders/passes/shadow.js +16 -0
  82. package/dist/shaders/stockings.d.ts +1 -1
  83. package/dist/shaders/stockings.d.ts.map +1 -1
  84. package/package.json +1 -1
  85. package/src/engine.ts +93 -438
  86. package/src/index.ts +3 -2
  87. package/src/shaders/{body.ts → materials/body.ts} +7 -28
  88. package/src/shaders/{cloth_rough.ts → materials/cloth_rough.ts} +4 -16
  89. package/src/shaders/{cloth_smooth.ts → materials/cloth_smooth.ts} +5 -17
  90. package/src/shaders/{face.ts → materials/face.ts} +21 -57
  91. package/src/shaders/{hair.ts → materials/hair.ts} +7 -27
  92. package/src/shaders/{metal.ts → materials/metal.ts} +15 -19
  93. package/src/shaders/materials/nodes.ts +483 -0
  94. package/src/shaders/passes/bloom.ts +121 -0
  95. package/src/shaders/passes/composite.ts +62 -0
  96. package/src/shaders/passes/ground.ts +94 -0
  97. package/src/shaders/passes/mipmap.ts +17 -0
  98. package/src/shaders/passes/outline.ts +84 -0
  99. package/src/shaders/passes/pick.ts +40 -0
  100. package/src/shaders/passes/shadow.ts +17 -0
  101. package/src/shaders/classify.ts +0 -25
  102. /package/src/shaders/{default.ts → materials/default.ts} +0 -0
  103. /package/src/shaders/{eye.ts → materials/eye.ts} +0 -0
  104. /package/src/shaders/{stockings.ts → materials/stockings.ts} +0 -0
package/dist/engine.js CHANGED
@@ -3,18 +3,33 @@ import { Mat4, Vec3 } from "./math";
3
3
  import { PmxLoader } from "./pmx-loader";
4
4
  import { Physics } from "./physics";
5
5
  import { createFetchAssetReader, createFileMapAssetReader, deriveBasePathFromPmxPath, fileListToMap, findFirstPmxFileInList, joinAssetPath, normalizeAssetPath, } from "./asset-reader";
6
- import { DEFAULT_SHADER_WGSL } from "./shaders/default";
6
+ import { DEFAULT_SHADER_WGSL } from "./shaders/materials/default";
7
+ import { FACE_SHADER_WGSL } from "./shaders/materials/face";
8
+ import { HAIR_SHADER_WGSL } from "./shaders/materials/hair";
9
+ import { CLOTH_SMOOTH_SHADER_WGSL } from "./shaders/materials/cloth_smooth";
10
+ import { CLOTH_ROUGH_SHADER_WGSL } from "./shaders/materials/cloth_rough";
11
+ import { METAL_SHADER_WGSL } from "./shaders/materials/metal";
12
+ import { BODY_SHADER_WGSL } from "./shaders/materials/body";
13
+ import { EYE_SHADER_WGSL } from "./shaders/materials/eye";
14
+ import { STOCKINGS_SHADER_WGSL } from "./shaders/materials/stockings";
7
15
  import { BRDF_LUT_SIZE, BRDF_LUT_BAKE_WGSL } from "./shaders/dfg_lut";
8
16
  import { LTC_MAG_LUT_SIZE, LTC_MAG_LUT_DATA } from "./shaders/ltc_mag_lut";
9
- import { FACE_SHADER_WGSL } from "./shaders/face";
10
- import { HAIR_SHADER_WGSL } from "./shaders/hair";
11
- import { CLOTH_SMOOTH_SHADER_WGSL } from "./shaders/cloth_smooth";
12
- import { CLOTH_ROUGH_SHADER_WGSL } from "./shaders/cloth_rough";
13
- import { METAL_SHADER_WGSL } from "./shaders/metal";
14
- import { BODY_SHADER_WGSL } from "./shaders/body";
15
- import { EYE_SHADER_WGSL } from "./shaders/eye";
16
- import { STOCKINGS_SHADER_WGSL } from "./shaders/stockings";
17
- import { resolvePreset } from "./shaders/classify";
17
+ import { SHADOW_DEPTH_SHADER_WGSL } from "./shaders/passes/shadow";
18
+ import { GROUND_SHADOW_SHADER_WGSL } from "./shaders/passes/ground";
19
+ import { OUTLINE_SHADER_WGSL } from "./shaders/passes/outline";
20
+ import { BLOOM_BLIT_SHADER_WGSL, BLOOM_DOWNSAMPLE_SHADER_WGSL, BLOOM_UPSAMPLE_SHADER_WGSL, } from "./shaders/passes/bloom";
21
+ import { COMPOSITE_SHADER_WGSL } from "./shaders/passes/composite";
22
+ import { PICK_SHADER_WGSL } from "./shaders/passes/pick";
23
+ import { MIPMAP_BLIT_SHADER_WGSL } from "./shaders/passes/mipmap";
24
+ function resolvePreset(materialName, map) {
25
+ if (!map)
26
+ return "default";
27
+ for (const [preset, names] of Object.entries(map)) {
28
+ if (names && names.includes(materialName))
29
+ return preset;
30
+ }
31
+ return "default";
32
+ }
18
33
  export const DEFAULT_BLOOM_OPTIONS = {
19
34
  enabled: true,
20
35
  threshold: 0.5,
@@ -50,7 +65,7 @@ export class Engine {
50
65
  this.lightData = new Float32Array(64);
51
66
  this.lightCount = 0;
52
67
  this.resizeObserver = null;
53
- // [exposure, gamma, _, _, bloomTint.x, bloomTint.y, bloomTint.z, bloomIntensity]
68
+ // [exposure, invGamma, _, _, bloomTint.x, bloomTint.y, bloomTint.z, bloomIntensity]
54
69
  this.compositeUniformData = new Float32Array(8);
55
70
  this.bloomBlitUniformData = new Float32Array(4);
56
71
  this.bloomUpsampleUniformData = new Float32Array(4);
@@ -200,7 +215,10 @@ export class Engine {
200
215
  const effIntensity = b.enabled ? b.intensity : 0.0;
201
216
  const u = this.compositeUniformData;
202
217
  u[0] = v.exposure;
203
- u[1] = Math.max(v.gamma, 1e-4);
218
+ // Store 1/gamma so the shader avoids a per-pixel divide. Safari's Metal
219
+ // compiler doesn't fold `pow(x, 1/g)` into identity when g=1, so also emit
220
+ // a uniform branch that skips the pow entirely in the common case.
221
+ u[1] = 1.0 / Math.max(v.gamma, 1e-4);
204
222
  u[2] = 0.0;
205
223
  u[3] = 0.0;
206
224
  u[4] = b.color.x;
@@ -640,21 +658,7 @@ export class Engine {
640
658
  });
641
659
  const shadowShader = this.device.createShaderModule({
642
660
  label: "shadow depth",
643
- code: /* wgsl */ `
644
- struct LightVP { viewProj: mat4x4f, };
645
- @group(0) @binding(0) var<uniform> lp: LightVP;
646
- @group(0) @binding(1) var<storage, read> skinMats: array<mat4x4f>;
647
- @vertex fn vs(@location(0) position: vec3f, @location(1) normal: vec3f, @location(2) uv: vec2f,
648
- @location(3) joints0: vec4<u32>, @location(4) weights0: vec4<f32>) -> @builtin(position) vec4f {
649
- let pos4 = vec4f(position, 1.0);
650
- let ws = weights0.x + weights0.y + weights0.z + weights0.w;
651
- let inv = select(1.0, 1.0 / ws, ws > 0.0001);
652
- let nw = select(vec4f(1.0,0.0,0.0,0.0), weights0 * inv, ws > 0.0001);
653
- var sp = vec4f(0.0);
654
- for (var i = 0u; i < 4u; i++) { sp += (skinMats[joints0[i]] * pos4) * nw[i]; }
655
- return lp.viewProj * vec4f(sp.xyz, 1.0);
656
- }
657
- `,
661
+ code: SHADOW_DEPTH_SHADER_WGSL,
658
662
  });
659
663
  this.shadowDepthPipeline = this.device.createRenderPipeline({
660
664
  label: "shadow depth pipeline",
@@ -711,99 +715,7 @@ export class Engine {
711
715
  });
712
716
  const groundShadowShader = this.device.createShaderModule({
713
717
  label: "ground shadow",
714
- code: /* wgsl */ `
715
- struct CameraUniforms { view: mat4x4f, projection: mat4x4f, viewPos: vec3f, _p: f32, };
716
- struct Light { direction: vec4f, color: vec4f, };
717
- struct LightUniforms { ambientColor: vec4f, lights: array<Light, 4>, };
718
- struct GroundShadowMat {
719
- diffuseColor: vec3f, fadeStart: f32,
720
- fadeEnd: f32, shadowStrength: f32, pcfTexel: f32, gridSpacing: f32,
721
- gridLineWidth: f32, gridLineOpacity: f32, noiseStrength: f32, _pad: f32,
722
- gridLineColor: vec3f, _pad2: f32,
723
- };
724
- struct LightVP { viewProj: mat4x4f, };
725
- @group(0) @binding(0) var<uniform> camera: CameraUniforms;
726
- @group(0) @binding(1) var<uniform> light: LightUniforms;
727
- @group(0) @binding(2) var shadowMap: texture_depth_2d;
728
- @group(0) @binding(3) var shadowSampler: sampler_comparison;
729
- @group(0) @binding(4) var<uniform> material: GroundShadowMat;
730
- @group(0) @binding(5) var<uniform> lightVP: LightVP;
731
-
732
- // Hash-based noise for frosted/matte surface
733
- fn hash2(p: vec2f) -> f32 {
734
- var p3 = fract(vec3f(p.x, p.y, p.x) * 0.1031);
735
- p3 += dot(p3, vec3f(p3.y + 33.33, p3.z + 33.33, p3.x + 33.33));
736
- return fract((p3.x + p3.y) * p3.z);
737
- }
738
- fn valueNoise(p: vec2f) -> f32 {
739
- let i = floor(p);
740
- let f = fract(p);
741
- let u = f * f * (3.0 - 2.0 * f);
742
- return mix(mix(hash2(i), hash2(i + vec2f(1.0, 0.0)), u.x),
743
- mix(hash2(i + vec2f(0.0, 1.0)), hash2(i + vec2f(1.0, 1.0)), u.x), u.y);
744
- }
745
- fn fbmNoise(p: vec2f) -> f32 {
746
- var v = 0.0;
747
- var a = 0.5;
748
- var pp = p;
749
- for (var i = 0; i < 4; i++) {
750
- v += a * valueNoise(pp);
751
- pp *= 2.0;
752
- a *= 0.5;
753
- }
754
- return v;
755
- }
756
-
757
- struct VO { @builtin(position) position: vec4f, @location(0) worldPos: vec3f, @location(1) normal: vec3f, };
758
- @vertex fn vs(@location(0) position: vec3f, @location(1) normal: vec3f, @location(2) uv: vec2f) -> VO {
759
- var o: VO; o.worldPos = position; o.normal = normal;
760
- o.position = camera.projection * camera.view * vec4f(position, 1.0); return o;
761
- }
762
- struct FSOut { @location(0) color: vec4f, @location(1) mask: f32 };
763
- @fragment fn fs(i: VO) -> FSOut {
764
- let n = normalize(i.normal);
765
- let centerDist = length(i.worldPos.xz);
766
- let edgeFade = 1.0 - smoothstep(0.0, 1.0, clamp((centerDist - material.fadeStart) / max(material.fadeEnd - material.fadeStart, 0.001), 0.0, 1.0));
767
-
768
- // Shadow sampling
769
- let lclip = lightVP.viewProj * vec4f(i.worldPos, 1.0);
770
- let ndc = lclip.xyz / max(lclip.w, 1e-6);
771
- let suv = vec2f(ndc.x * 0.5 + 0.5, 0.5 - ndc.y * 0.5);
772
- let suv_c = clamp(suv, vec2f(0.02), vec2f(0.98));
773
- let st = material.pcfTexel;
774
- let compareZ = ndc.z - 0.0035;
775
- var vis = 0.0;
776
- for (var y = -2; y <= 2; y++) {
777
- for (var x = -2; x <= 2; x++) {
778
- vis += textureSampleCompare(shadowMap, shadowSampler, suv_c + vec2f(f32(x), f32(y)) * st, compareZ);
779
- }
780
- }
781
- vis *= 0.04;
782
-
783
- // Frosted/matte micro-texture (磨砂)
784
- let noiseVal = fbmNoise(i.worldPos.xz * 3.0);
785
- let noiseTint = 1.0 + (noiseVal - 0.5) * material.noiseStrength;
786
-
787
- // Grid lines — anti-aliased via screen-space derivatives
788
- let gp = i.worldPos.xz / material.gridSpacing;
789
- let gridFrac = abs(fract(gp - 0.5) - 0.5);
790
- let gridDeriv = fwidth(gp);
791
- let halfLine = material.gridLineWidth * 0.5;
792
- let gridLine = 1.0 - min(
793
- smoothstep(halfLine - gridDeriv.x, halfLine + gridDeriv.x, gridFrac.x),
794
- smoothstep(halfLine - gridDeriv.y, halfLine + gridDeriv.y, gridFrac.y)
795
- );
796
- 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);
797
- let dark = (1.0 - vis) * material.shadowStrength;
798
- var baseColor = material.diffuseColor * sun * (1.0 - dark * 0.65);
799
- baseColor *= noiseTint;
800
- let finalColor = mix(baseColor, material.gridLineColor, gridLine * material.gridLineOpacity * edgeFade);
801
- var out: FSOut;
802
- out.color = vec4f(finalColor * edgeFade, edgeFade);
803
- out.mask = 0.0;
804
- return out;
805
- }
806
- `,
718
+ code: GROUND_SHADOW_SHADER_WGSL,
807
719
  });
808
720
  this.groundShadowPipeline = this.createRenderPipeline({
809
721
  label: "ground shadow pipeline",
@@ -843,90 +755,7 @@ export class Engine {
843
755
  });
844
756
  const outlineShaderModule = this.device.createShaderModule({
845
757
  label: "outline shaders",
846
- code: /* wgsl */ `
847
- struct CameraUniforms {
848
- view: mat4x4f,
849
- projection: mat4x4f,
850
- viewPos: vec3f,
851
- _padding: f32,
852
- };
853
-
854
- struct MaterialUniforms {
855
- edgeColor: vec4f,
856
- edgeSize: f32,
857
- _padding1: f32,
858
- _padding2: f32,
859
- _padding3: f32,
860
- };
861
-
862
- // group 0: per-frame
863
- @group(0) @binding(0) var<uniform> camera: CameraUniforms;
864
- // group 1: per-instance
865
- @group(1) @binding(0) var<storage, read> skinMats: array<mat4x4f>;
866
- // group 2: per-material
867
- @group(2) @binding(0) var<uniform> material: MaterialUniforms;
868
-
869
- struct VertexOutput {
870
- @builtin(position) position: vec4f,
871
- };
872
-
873
- @vertex fn vs(
874
- @location(0) position: vec3f,
875
- @location(1) normal: vec3f,
876
- @location(3) joints0: vec4<u32>,
877
- @location(4) weights0: vec4<f32>
878
- ) -> VertexOutput {
879
- var output: VertexOutput;
880
- let pos4 = vec4f(position, 1.0);
881
-
882
- let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;
883
- let invWeightSum = select(1.0, 1.0 / weightSum, weightSum > 0.0001);
884
- let normalizedWeights = select(vec4f(1.0, 0.0, 0.0, 0.0), weights0 * invWeightSum, weightSum > 0.0001);
885
-
886
- var skinnedPos = vec4f(0.0, 0.0, 0.0, 0.0);
887
- var skinnedNrm = vec3f(0.0, 0.0, 0.0);
888
- for (var i = 0u; i < 4u; i++) {
889
- let j = joints0[i];
890
- let w = normalizedWeights[i];
891
- let m = skinMats[j];
892
- skinnedPos += (m * pos4) * w;
893
- let r3 = mat3x3f(m[0].xyz, m[1].xyz, m[2].xyz);
894
- skinnedNrm += (r3 * normal) * w;
895
- }
896
- let worldPos = skinnedPos.xyz;
897
- let worldNormal = normalize(skinnedNrm);
898
-
899
- // Screen-space outline extrusion — MMD-style pixel-stable edge line.
900
- // 1. Project position and normal-as-direction to clip space.
901
- // 2. Normalize the 2D clip-space normal, aspect-compensated so "one pixel horizontally"
902
- // matches "one pixel vertically" (otherwise wide viewports squash the outline in X).
903
- // 3. Offset clip-space xy by (normal * edgeSize * edgeScale), then multiply by w
904
- // so the perspective divide cancels out → offset stays constant in NDC regardless
905
- // of depth, matching how MMD / babylon-mmd style outlines look identical when zooming.
906
- // 4. edgeScale is in NDC-y units per PMX edgeSize. ≈ 0.006 gives ~3px at 1080p; it's
907
- // tied to viewport HEIGHT so resizing the window keeps pixel thickness stable.
908
- let viewProj = camera.projection * camera.view;
909
- let clipPos = viewProj * vec4f(worldPos, 1.0);
910
- let clipNormal = (viewProj * vec4f(worldNormal, 0.0)).xy;
911
- // projection is column-major: proj[0][0] = 1/(aspect·tan(fov/2)), proj[1][1] = 1/tan(fov/2).
912
- // Ratio proj[1][1]/proj[0][0] recovers the viewport aspect (width/height).
913
- let aspect = camera.projection[1][1] / camera.projection[0][0];
914
- let pixelDir = normalize(vec2f(clipNormal.x * aspect, clipNormal.y));
915
- let ndcDir = vec2f(pixelDir.x / aspect, pixelDir.y);
916
- let edgeScale = 0.0016;
917
- let offset = ndcDir * material.edgeSize * edgeScale * clipPos.w;
918
- output.position = vec4f(clipPos.xy + offset, clipPos.z, clipPos.w);
919
- return output;
920
- }
921
-
922
- struct FSOut { @location(0) color: vec4f, @location(1) mask: f32 };
923
- @fragment fn fs() -> FSOut {
924
- var out: FSOut;
925
- out.color = material.edgeColor;
926
- out.mask = 1.0;
927
- return out;
928
- }
929
- `,
758
+ code: OUTLINE_SHADER_WGSL,
930
759
  });
931
760
  this.outlinePipeline = this.createRenderPipeline({
932
761
  label: "outline pipeline",
@@ -987,127 +816,17 @@ export class Engine {
987
816
  { binding: 3, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } },
988
817
  ],
989
818
  });
990
- const bloomFullscreenVs = /* wgsl */ `
991
- @vertex fn vs(@builtin(vertex_index) vi: u32) -> @builtin(position) vec4f {
992
- let x = f32((vi & 1u) << 2u) - 1.0;
993
- let y = f32((vi & 2u) << 1u) - 1.0;
994
- return vec4f(x, y, 0.0, 1.0);
995
- }
996
- `;
997
- // Blit: full-res HDR → half-res. Karis 4-tap firefly average + EEVEE quadratic knee threshold + clamp.
998
819
  const bloomBlitShader = this.device.createShaderModule({
999
820
  label: "bloom blit (Karis prefilter)",
1000
- code: `${bloomFullscreenVs}
1001
- @group(0) @binding(0) var hdrTex: texture_2d<f32>;
1002
- @group(0) @binding(1) var<uniform> prefilter: vec4<f32>; // threshold, knee, clamp, _unused
1003
- @group(0) @binding(2) var maskTex: texture_2d<f32>;
1004
-
1005
- fn luminance(c: vec3f) -> f32 {
1006
- return dot(max(c, vec3f(0.0)), vec3f(0.2126, 0.7152, 0.0722));
1007
- }
1008
- fn fetch(c: vec2<i32>, clampV: f32) -> vec3f {
1009
- let d = vec2<i32>(textureDimensions(hdrTex));
1010
- let cc = clamp(c, vec2<i32>(0), d - vec2<i32>(1));
1011
- let s = textureLoad(hdrTex, cc, 0);
1012
- // Scene pass uses src-alpha blend with clear alpha 0 → premultiplied. Unpremultiply.
1013
- let rgb = max(s.rgb / max(s.a, 1e-6), vec3f(0.0));
1014
- // Bloom mask: MRT r8unorm written by material shaders (1.0 = bloom, 0.0 = skip).
1015
- let mask = textureLoad(maskTex, cc, 0).r;
1016
- let masked = rgb * mask;
1017
- // Blender: clamp each tap BEFORE Karis average (eevee_bloom: color = min(clampIntensity, color)).
1018
- return select(masked, min(masked, vec3f(clampV)), clampV > 0.0);
1019
- }
1020
-
1021
- @fragment fn fs(@builtin(position) p: vec4f) -> @location(0) vec4f {
1022
- let dst = vec2<i32>(p.xy - vec2f(0.5));
1023
- let base = dst * 2;
1024
- let clampV = prefilter.z;
1025
- let a = fetch(base + vec2<i32>(0, 0), clampV);
1026
- let b = fetch(base + vec2<i32>(1, 0), clampV);
1027
- let c = fetch(base + vec2<i32>(0, 1), clampV);
1028
- let d = fetch(base + vec2<i32>(1, 1), clampV);
1029
- // Karis partial average: weight each tap by 1/(1+luma) — suppresses fireflies.
1030
- let wa = 1.0 / (1.0 + luminance(a));
1031
- let wb = 1.0 / (1.0 + luminance(b));
1032
- let wc = 1.0 / (1.0 + luminance(c));
1033
- let wd = 1.0 / (1.0 + luminance(d));
1034
- let avg = (a * wa + b * wb + c * wc + d * wd) / max(wa + wb + wc + wd, 1e-6);
1035
- // EEVEE quadratic threshold (brightness = max-channel, then soft-knee curve).
1036
- let bright = max(avg.r, max(avg.g, avg.b));
1037
- let soft = clamp(bright - prefilter.x + prefilter.y, 0.0, 2.0 * prefilter.y);
1038
- let q = (soft * soft) / (4.0 * max(prefilter.y, 1e-4) + 1e-6);
1039
- let contrib = max(q, bright - prefilter.x) / max(bright, 1e-4);
1040
- return vec4f(max(avg * contrib, vec3f(0.0)), 1.0);
1041
- }
1042
- `,
821
+ code: BLOOM_BLIT_SHADER_WGSL,
1043
822
  });
1044
- // Downsample: Jimenez/COD 13-tap dual-box — 5 weighted 2×2 averages, rejects nyquist ringing.
1045
823
  const bloomDownsampleShader = this.device.createShaderModule({
1046
824
  label: "bloom downsample 13-tap",
1047
- code: `${bloomFullscreenVs}
1048
- @group(0) @binding(0) var srcTex: texture_2d<f32>;
1049
- @group(0) @binding(1) var srcSamp: sampler;
1050
-
1051
- fn samp(uv: vec2f, off: vec2f) -> vec3f {
1052
- return textureSampleLevel(srcTex, srcSamp, uv + off, 0.0).rgb;
1053
- }
1054
-
1055
- @fragment fn fs(@builtin(position) p: vec4f) -> @location(0) vec4f {
1056
- let srcDims = vec2f(textureDimensions(srcTex));
1057
- let t = 1.0 / srcDims;
1058
- // fragCoord.xy reports pixel centers (e.g. 0.5,0.5 for first pixel) — divide by dst dims directly.
1059
- let dstDims = srcDims * 0.5;
1060
- let uv = p.xy / max(dstDims, vec2f(1.0));
1061
- let A = samp(uv, t * vec2f(-2.0, -2.0));
1062
- let B = samp(uv, t * vec2f( 0.0, -2.0));
1063
- let C = samp(uv, t * vec2f( 2.0, -2.0));
1064
- let D = samp(uv, t * vec2f(-1.0, -1.0));
1065
- let E = samp(uv, t * vec2f( 1.0, -1.0));
1066
- let F = samp(uv, t * vec2f(-2.0, 0.0));
1067
- let G = samp(uv, t * vec2f( 0.0, 0.0));
1068
- let H = samp(uv, t * vec2f( 2.0, 0.0));
1069
- let I = samp(uv, t * vec2f(-1.0, 1.0));
1070
- let J = samp(uv, t * vec2f( 1.0, 1.0));
1071
- let K = samp(uv, t * vec2f(-2.0, 2.0));
1072
- let L = samp(uv, t * vec2f( 0.0, 2.0));
1073
- let M = samp(uv, t * vec2f( 2.0, 2.0));
1074
- var o = (D + E + I + J) * (0.5 / 4.0);
1075
- o = o + (A + B + G + F) * (0.125 / 4.0);
1076
- o = o + (B + C + H + G) * (0.125 / 4.0);
1077
- o = o + (F + G + L + K) * (0.125 / 4.0);
1078
- o = o + (G + H + M + L) * (0.125 / 4.0);
1079
- return vec4f(o, 1.0);
1080
- }
1081
- `,
825
+ code: BLOOM_DOWNSAMPLE_SHADER_WGSL,
1082
826
  });
1083
- // Upsample: 9-tap tent, progressively added to matching downsample mip. Blender radius = sample scale.
1084
827
  const bloomUpsampleShader = this.device.createShaderModule({
1085
828
  label: "bloom upsample 9-tap tent",
1086
- code: `${bloomFullscreenVs}
1087
- @group(0) @binding(0) var srcTex: texture_2d<f32>; // coarser accumulator
1088
- @group(0) @binding(1) var baseTex: texture_2d<f32>; // matching downsample mip
1089
- @group(0) @binding(2) var srcSamp: sampler;
1090
- @group(0) @binding(3) var<uniform> upU: vec4<f32>; // sampleScale, _, _, _
1091
-
1092
- @fragment fn fs(@builtin(position) p: vec4f) -> @location(0) vec4f {
1093
- let srcDims = vec2f(textureDimensions(srcTex));
1094
- let baseDims = vec2f(textureDimensions(baseTex));
1095
- let uv = p.xy / max(baseDims, vec2f(1.0));
1096
- let t = upU.x / srcDims;
1097
- var o = textureSampleLevel(srcTex, srcSamp, uv + t * vec2f(-1.0, -1.0), 0.0).rgb * 1.0;
1098
- o = o + textureSampleLevel(srcTex, srcSamp, uv + t * vec2f( 0.0, -1.0), 0.0).rgb * 2.0;
1099
- o = o + textureSampleLevel(srcTex, srcSamp, uv + t * vec2f( 1.0, -1.0), 0.0).rgb * 1.0;
1100
- o = o + textureSampleLevel(srcTex, srcSamp, uv + t * vec2f(-1.0, 0.0), 0.0).rgb * 2.0;
1101
- o = o + textureSampleLevel(srcTex, srcSamp, uv + t * vec2f( 0.0, 0.0), 0.0).rgb * 4.0;
1102
- o = o + textureSampleLevel(srcTex, srcSamp, uv + t * vec2f( 1.0, 0.0), 0.0).rgb * 2.0;
1103
- o = o + textureSampleLevel(srcTex, srcSamp, uv + t * vec2f(-1.0, 1.0), 0.0).rgb * 1.0;
1104
- o = o + textureSampleLevel(srcTex, srcSamp, uv + t * vec2f( 0.0, 1.0), 0.0).rgb * 2.0;
1105
- o = o + textureSampleLevel(srcTex, srcSamp, uv + t * vec2f( 1.0, 1.0), 0.0).rgb * 1.0;
1106
- o = o * (1.0 / 16.0);
1107
- let base = textureSampleLevel(baseTex, srcSamp, uv, 0.0).rgb;
1108
- return vec4f(o + base, 1.0);
1109
- }
1110
- `,
829
+ code: BLOOM_UPSAMPLE_SHADER_WGSL,
1111
830
  });
1112
831
  const bloomBlitLayout = this.device.createPipelineLayout({ bindGroupLayouts: [this.bloomBlitBindGroupLayout] });
1113
832
  const bloomDownLayout = this.device.createPipelineLayout({
@@ -1154,67 +873,25 @@ export class Engine {
1154
873
  });
1155
874
  const compositeShader = this.device.createShaderModule({
1156
875
  label: "composite shader",
1157
- code: /* wgsl */ `
1158
- @group(0) @binding(0) var hdrTex: texture_2d<f32>;
1159
- @group(0) @binding(1) var bloomTex: texture_2d<f32>; // bloomUpTexture mip 0 (full pyramid top)
1160
- @group(0) @binding(2) var bloomSamp: sampler;
1161
- @group(0) @binding(3) var<uniform> viewU: array<vec4<f32>, 2>;
1162
- // viewU[0] = (exposure, gamma, _, _); viewU[1] = (tint.rgb, intensity)
1163
-
1164
- fn filmic(x: f32) -> f32 {
1165
- // Re-fit against Blender 3.6 Filmic MHC anchors (sobotka/filmic-blender
1166
- // look_medium-high-contrast.spi1d). Previous curve was compressed:
1167
- // midtones too bright, highlights too dim — flattened contrast, read
1168
- // as "washed-out" on saturated surfaces (hair especially).
1169
- // Reference checkpoints: linear 0.18 → ~0.395, linear 1.0 → ~0.83.
1170
- var lut = array<f32, 14>(
1171
- 0.0028, 0.0068, 0.0151, 0.0313, 0.0610, 0.1120, 0.1920,
1172
- 0.3060, 0.4590, 0.6310, 0.8200, 0.9070, 0.9620, 0.9890
1173
- );
1174
- let t = clamp(log2(max(x, 1e-10)) + 10.0, 0.0, 13.0);
1175
- let i = u32(t);
1176
- let j = min(i + 1u, 13u);
1177
- return mix(lut[i], lut[j], t - f32(i));
1178
- }
1179
-
1180
- @vertex fn vs(@builtin(vertex_index) vi: u32) -> @builtin(position) vec4f {
1181
- let x = f32((vi & 1u) << 2u) - 1.0;
1182
- let y = f32((vi & 2u) << 1u) - 1.0;
1183
- return vec4f(x, y, 0.0, 1.0);
1184
- }
1185
-
1186
- @fragment fn fs(@builtin(position) fragCoord: vec4f) -> @location(0) vec4f {
1187
- let hdr = textureLoad(hdrTex, vec2<i32>(fragCoord.xy), 0);
1188
- let a = max(hdr.a, 1e-6);
1189
- let straight = hdr.rgb / a;
1190
- let fullSz = vec2f(textureDimensions(hdrTex));
1191
- let bloomSz = vec2f(textureDimensions(bloomTex));
1192
- // Bloom is at half-res (pyramid mip 0). Sampler interpolates back to full-res UVs.
1193
- // fragCoord.xy is already at pixel center (e.g. 0.5, 0.5 for first pixel).
1194
- let bloomUv = fragCoord.xy / max(fullSz, vec2f(1.0));
1195
- let tint = viewU[1].xyz;
1196
- let intensity = viewU[1].w;
1197
- let bloom = textureSampleLevel(bloomTex, bloomSamp, bloomUv, 0.0).rgb * tint * intensity;
1198
- let combined = straight + bloom;
1199
- let exposed = combined * exp2(viewU[0].x);
1200
- let tm = vec3f(filmic(exposed.r), filmic(exposed.g), filmic(exposed.b));
1201
- let g = max(viewU[0].y, 1e-4);
1202
- let disp = pow(max(tm, vec3f(0.0)), vec3f(1.0 / g));
1203
- return vec4f(disp * hdr.a, hdr.a);
1204
- }
1205
- `,
876
+ code: COMPOSITE_SHADER_WGSL,
1206
877
  });
1207
- this.compositePipeline = this.device.createRenderPipeline({
1208
- label: "composite pipeline",
1209
- layout: this.device.createPipelineLayout({ bindGroupLayouts: [this.compositeBindGroupLayout] }),
878
+ const compositePipelineLayout = this.device.createPipelineLayout({
879
+ bindGroupLayouts: [this.compositeBindGroupLayout],
880
+ });
881
+ const makeCompositePipeline = (applyGamma, label) => this.device.createRenderPipeline({
882
+ label,
883
+ layout: compositePipelineLayout,
1210
884
  vertex: { module: compositeShader, entryPoint: "vs" },
1211
885
  fragment: {
1212
886
  module: compositeShader,
1213
887
  entryPoint: "fs",
888
+ constants: { APPLY_GAMMA: applyGamma ? 1 : 0 },
1214
889
  targets: [{ format: this.presentationFormat }],
1215
890
  },
1216
891
  primitive: { topology: "triangle-list" },
1217
892
  });
893
+ this.compositePipelineIdentity = makeCompositePipeline(false, "composite pipeline (gamma=1)");
894
+ this.compositePipelineGamma = makeCompositePipeline(true, "composite pipeline (gamma!=1)");
1218
895
  this.bloomPassDescriptor = {
1219
896
  label: "bloom pass",
1220
897
  colorAttachments: [
@@ -1226,47 +903,9 @@ export class Engine {
1226
903
  },
1227
904
  ],
1228
905
  };
1229
- // GPU picking: encode (modelIndex, materialIndex) as color
1230
906
  const pickShaderModule = this.device.createShaderModule({
1231
907
  label: "pick shader",
1232
- code: /* wgsl */ `
1233
- struct CameraUniforms {
1234
- view: mat4x4f,
1235
- projection: mat4x4f,
1236
- viewPos: vec3f,
1237
- _padding: f32,
1238
- };
1239
- struct PickId {
1240
- modelId: f32,
1241
- materialId: f32,
1242
- _p1: f32,
1243
- _p2: f32,
1244
- };
1245
-
1246
- @group(0) @binding(0) var<uniform> camera: CameraUniforms;
1247
- @group(1) @binding(0) var<storage, read> skinMats: array<mat4x4f>;
1248
- @group(2) @binding(0) var<uniform> pickId: PickId;
1249
-
1250
- @vertex fn vs(
1251
- @location(0) position: vec3f,
1252
- @location(1) normal: vec3f,
1253
- @location(2) uv: vec2f,
1254
- @location(3) joints0: vec4<u32>,
1255
- @location(4) weights0: vec4<f32>
1256
- ) -> @builtin(position) vec4f {
1257
- let pos4 = vec4f(position, 1.0);
1258
- let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;
1259
- let invWeightSum = select(1.0, 1.0 / weightSum, weightSum > 0.0001);
1260
- let nw = select(vec4f(1.0, 0.0, 0.0, 0.0), weights0 * invWeightSum, weightSum > 0.0001);
1261
- var sp = vec4f(0.0);
1262
- for (var i = 0u; i < 4u; i++) { sp += (skinMats[joints0[i]] * pos4) * nw[i]; }
1263
- return camera.projection * camera.view * vec4f(sp.xyz, 1.0);
1264
- }
1265
-
1266
- @fragment fn fs() -> @location(0) vec4f {
1267
- return vec4f(pickId.modelId / 255.0, pickId.materialId / 255.0, 0.0, 1.0);
1268
- }
1269
- `,
908
+ code: PICK_SHADER_WGSL,
1270
909
  });
1271
910
  this.pickPerFrameBindGroupLayout = this.device.createBindGroupLayout({
1272
911
  label: "pick per-frame layout",
@@ -1400,19 +1039,23 @@ export class Engine {
1400
1039
  usage: GPUTextureUsage.RENDER_ATTACHMENT,
1401
1040
  });
1402
1041
  const depthTextureView = this.depthTexture.createView();
1042
+ // storeOp="discard" on MSAA views keeps per-sample data in Apple TBDR tile memory —
1043
+ // only the resolveTarget (hdrResolveTexture / maskResolveView) gets written to RAM.
1044
+ // With storeOp="store" Safari's Metal backend spills the full MS buffer every frame
1045
+ // (rgba16f × 4 samples on a 4K canvas ≈ 256 MB/frame of dead bandwidth).
1403
1046
  const colorAttachment = {
1404
1047
  view: this.multisampleTexture.createView(),
1405
1048
  resolveTarget: this.hdrResolveTexture.createView(),
1406
1049
  clearValue: { r: 0, g: 0, b: 0, a: 0 },
1407
1050
  loadOp: "clear",
1408
- storeOp: "store",
1051
+ storeOp: "discard",
1409
1052
  };
1410
1053
  const maskAttachment = {
1411
1054
  view: this.multisampleMaskTexture.createView(),
1412
1055
  resolveTarget: this.maskResolveView,
1413
1056
  clearValue: { r: 0, g: 0, b: 0, a: 0 },
1414
1057
  loadOp: "clear",
1415
- storeOp: "store",
1058
+ storeOp: "discard",
1416
1059
  };
1417
1060
  this.renderPassDescriptor = {
1418
1061
  label: "renderPass",
@@ -1421,7 +1064,8 @@ export class Engine {
1421
1064
  view: depthTextureView,
1422
1065
  depthClearValue: 1.0,
1423
1066
  depthLoadOp: "clear",
1424
- depthStoreOp: "store",
1067
+ // Main-pass depth is not sampled later (shadow uses its own map, composite is depthless).
1068
+ depthStoreOp: "discard",
1425
1069
  stencilClearValue: 0,
1426
1070
  stencilLoadOp: "clear",
1427
1071
  stencilStoreOp: "discard",
@@ -2213,20 +1857,7 @@ export class Engine {
2213
1857
  });
2214
1858
  const module = this.device.createShaderModule({
2215
1859
  label: "mipmap blit",
2216
- code: /* wgsl */ `
2217
- @group(0) @binding(0) var src: texture_2d<f32>;
2218
- @group(0) @binding(1) var samp: sampler;
2219
- @vertex fn vs(@builtin(vertex_index) vi: u32) -> @builtin(position) vec4f {
2220
- let x = f32((vi & 1u) << 2u) - 1.0;
2221
- let y = f32((vi & 2u) << 1u) - 1.0;
2222
- return vec4f(x, y, 0.0, 1.0);
2223
- }
2224
- @fragment fn fs(@builtin(position) p: vec4f) -> @location(0) vec4f {
2225
- let dstDims = vec2f(textureDimensions(src)) * 0.5;
2226
- let uv = p.xy / max(dstDims, vec2f(1.0));
2227
- return textureSampleLevel(src, samp, uv, 0.0);
2228
- }
2229
- `,
1860
+ code: MIPMAP_BLIT_SHADER_WGSL,
2230
1861
  });
2231
1862
  this.mipBlitPipeline = this.device.createRenderPipeline({
2232
1863
  label: "mipmap blit pipeline",
@@ -2441,7 +2072,8 @@ export class Engine {
2441
2072
  const compositeAttachment = this.compositePassDescriptor.colorAttachments[0];
2442
2073
  compositeAttachment.view = this.context.getCurrentTexture().createView();
2443
2074
  const cpass = encoder.beginRenderPass(this.compositePassDescriptor);
2444
- cpass.setPipeline(this.compositePipeline);
2075
+ const compositePipeline = this.viewTransform.gamma === 1.0 ? this.compositePipelineIdentity : this.compositePipelineGamma;
2076
+ cpass.setPipeline(compositePipeline);
2445
2077
  cpass.setBindGroup(0, this.compositeBindGroup);
2446
2078
  cpass.draw(3);
2447
2079
  cpass.end();
package/dist/index.d.ts CHANGED
@@ -1,9 +1,8 @@
1
- export { Engine, DEFAULT_BLOOM_OPTIONS, DEFAULT_VIEW_TRANSFORM, type EngineStats, type EngineOptions, type BloomOptions, type ViewTransformOptions, type LoadModelFromFilesOptions, } from "./engine";
1
+ export { Engine, DEFAULT_BLOOM_OPTIONS, DEFAULT_VIEW_TRANSFORM, type EngineStats, type EngineOptions, type BloomOptions, type ViewTransformOptions, type LoadModelFromFilesOptions, type MaterialPreset, type MaterialPresetMap, } from "./engine";
2
2
  export { parsePmxFolderInput, pmxFileAtRelativePath, type PmxFolderInputResult } from "./folder-upload";
3
3
  export { Model } from "./model";
4
4
  export { Vec3, Quat, Mat4 } from "./math";
5
5
  export type { AnimationClip, AnimationPlayOptions, AnimationProgress, BoneKeyframe, MorphKeyframe, BoneInterpolation, ControlPoint, } from "./animation";
6
6
  export { FPS } from "./animation";
7
7
  export { Physics, type PhysicsOptions } from "./physics";
8
- export type { MaterialPreset, MaterialPresetMap } from "./shaders/classify";
9
8
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,MAAM,EACN,qBAAqB,EACrB,sBAAsB,EACtB,KAAK,WAAW,EAChB,KAAK,aAAa,EAClB,KAAK,YAAY,EACjB,KAAK,oBAAoB,EACzB,KAAK,yBAAyB,GAC/B,MAAM,UAAU,CAAA;AACjB,OAAO,EAAE,mBAAmB,EAAE,qBAAqB,EAAE,KAAK,oBAAoB,EAAE,MAAM,iBAAiB,CAAA;AACvG,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAA;AAC/B,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAA;AACzC,YAAY,EACV,aAAa,EACb,oBAAoB,EACpB,iBAAiB,EACjB,YAAY,EACZ,aAAa,EACb,iBAAiB,EACjB,YAAY,GACb,MAAM,aAAa,CAAA;AACpB,OAAO,EAAE,GAAG,EAAE,MAAM,aAAa,CAAA;AACjC,OAAO,EAAE,OAAO,EAAE,KAAK,cAAc,EAAE,MAAM,WAAW,CAAA;AACxD,YAAY,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,MAAM,EACN,qBAAqB,EACrB,sBAAsB,EACtB,KAAK,WAAW,EAChB,KAAK,aAAa,EAClB,KAAK,YAAY,EACjB,KAAK,oBAAoB,EACzB,KAAK,yBAAyB,EAC9B,KAAK,cAAc,EACnB,KAAK,iBAAiB,GACvB,MAAM,UAAU,CAAA;AACjB,OAAO,EAAE,mBAAmB,EAAE,qBAAqB,EAAE,KAAK,oBAAoB,EAAE,MAAM,iBAAiB,CAAA;AACvG,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAA;AAC/B,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAA;AACzC,YAAY,EACV,aAAa,EACb,oBAAoB,EACpB,iBAAiB,EACjB,YAAY,EACZ,aAAa,EACb,iBAAiB,EACjB,YAAY,GACb,MAAM,aAAa,CAAA;AACpB,OAAO,EAAE,GAAG,EAAE,MAAM,aAAa,CAAA;AACjC,OAAO,EAAE,OAAO,EAAE,KAAK,cAAc,EAAE,MAAM,WAAW,CAAA"}