reze-engine 0.19.0 → 0.21.0

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 (65) hide show
  1. package/README.md +300 -252
  2. package/dist/engine.d.ts +12 -25
  3. package/dist/engine.d.ts.map +1 -1
  4. package/dist/engine.js +97 -312
  5. package/dist/graph/compile.d.ts +3 -3
  6. package/dist/graph/compile.d.ts.map +1 -1
  7. package/dist/graph/presets/body.d.ts +2 -2
  8. package/dist/graph/presets/body.d.ts.map +1 -1
  9. package/dist/graph/presets/body.js +1 -1
  10. package/dist/graph/presets/cloth_rough.d.ts +2 -2
  11. package/dist/graph/presets/cloth_rough.d.ts.map +1 -1
  12. package/dist/graph/presets/cloth_rough.js +1 -1
  13. package/dist/graph/presets/cloth_smooth.d.ts +2 -2
  14. package/dist/graph/presets/cloth_smooth.d.ts.map +1 -1
  15. package/dist/graph/presets/cloth_smooth.js +1 -1
  16. package/dist/graph/presets/default.d.ts +2 -2
  17. package/dist/graph/presets/default.d.ts.map +1 -1
  18. package/dist/graph/presets/default.js +13 -6
  19. package/dist/graph/presets/eye.d.ts +2 -2
  20. package/dist/graph/presets/eye.d.ts.map +1 -1
  21. package/dist/graph/presets/eye.js +1 -1
  22. package/dist/graph/presets/face.d.ts +2 -2
  23. package/dist/graph/presets/face.d.ts.map +1 -1
  24. package/dist/graph/presets/face.js +1 -1
  25. package/dist/graph/presets/hair.d.ts +2 -2
  26. package/dist/graph/presets/hair.d.ts.map +1 -1
  27. package/dist/graph/presets/hair.js +1 -1
  28. package/dist/graph/presets/metal.d.ts +2 -2
  29. package/dist/graph/presets/metal.d.ts.map +1 -1
  30. package/dist/graph/presets/metal.js +1 -1
  31. package/dist/graph/presets/stockings.d.ts +2 -2
  32. package/dist/graph/presets/stockings.d.ts.map +1 -1
  33. package/dist/graph/presets/stockings.js +1 -1
  34. package/dist/graph/registry.d.ts.map +1 -1
  35. package/dist/graph/registry.js +8 -0
  36. package/dist/graph/render-class.js +1 -1
  37. package/dist/graph/schema.d.ts +1 -1
  38. package/dist/graph/schema.d.ts.map +1 -1
  39. package/dist/graph/schema.js +1 -1
  40. package/dist/graph/style-group.d.ts +3 -3
  41. package/dist/graph/style-group.d.ts.map +1 -1
  42. package/dist/graph/style-group.js +1 -1
  43. package/dist/index.d.ts +1 -1
  44. package/dist/index.d.ts.map +1 -1
  45. package/dist/tga-loader.d.ts +7 -0
  46. package/dist/tga-loader.d.ts.map +1 -0
  47. package/dist/tga-loader.js +154 -0
  48. package/package.json +2 -2
  49. package/src/engine.ts +4488 -4718
  50. package/src/graph/compile.ts +3 -3
  51. package/src/graph/presets/body.ts +3 -3
  52. package/src/graph/presets/cloth_rough.ts +3 -3
  53. package/src/graph/presets/cloth_smooth.ts +3 -3
  54. package/src/graph/presets/default.ts +15 -8
  55. package/src/graph/presets/eye.ts +3 -3
  56. package/src/graph/presets/face.ts +3 -3
  57. package/src/graph/presets/hair.ts +3 -3
  58. package/src/graph/presets/metal.ts +3 -3
  59. package/src/graph/presets/stockings.ts +3 -3
  60. package/src/graph/registry.ts +8 -0
  61. package/src/graph/render-class.ts +1 -1
  62. package/src/graph/schema.ts +2 -2
  63. package/src/graph/style-group.ts +4 -4
  64. package/src/index.ts +62 -62
  65. package/src/tga-loader.ts +154 -0
package/dist/engine.js CHANGED
@@ -1,19 +1,10 @@
1
1
  import { Camera } from "./camera";
2
2
  import { Mat4, Quat, Vec3 } from "./math";
3
3
  import { MORPH_COMPUTE_WGSL } from "./shaders/passes/morph";
4
+ import { decodeTga } from "./tga-loader";
4
5
  import { PmxLoader } from "./pmx-loader";
5
6
  import { RezePhysics } from "./physics";
6
7
  import { createFetchAssetReader, createFileMapAssetReader, deriveBasePathFromPmxPath, fileListToMap, findFirstPmxFileInList, joinAssetPath, normalizeAssetPath, } from "./asset-reader";
7
- import { DEFAULT_SHADER_WGSL } from "./shaders/materials/default";
8
- import { FACE_SHADER_WGSL } from "./shaders/materials/face";
9
- import { HAIR_SHADER_WGSL } from "./shaders/materials/hair";
10
- import { CLOTH_SMOOTH_SHADER_WGSL } from "./shaders/materials/cloth_smooth";
11
- import { CLOTH_ROUGH_SHADER_WGSL } from "./shaders/materials/cloth_rough";
12
- import { METAL_SHADER_WGSL } from "./shaders/materials/metal";
13
- import { BODY_SHADER_WGSL } from "./shaders/materials/body";
14
- import { EYE_SHADER_WGSL } from "./shaders/materials/eye";
15
- import { STOCKINGS_SHADER_WGSL } from "./shaders/materials/stockings";
16
- import { MMD_CLASSIC_SHADER_WGSL } from "./shaders/materials/mmd_classic";
17
8
  import { BRDF_LUT_SIZE, BRDF_LUT_BAKE_WGSL } from "./shaders/dfg_lut";
18
9
  import { LTC_MAG_LUT_SIZE, LTC_MAG_LUT_DATA } from "./shaders/ltc_mag_lut";
19
10
  import { SHADOW_DEPTH_SHADER_WGSL } from "./shaders/passes/shadow";
@@ -874,46 +865,6 @@ export class Engine {
874
865
  const sceneTargets = [standardBlend, maskBlend];
875
866
  this.sceneTargets = sceneTargets;
876
867
  this.fullVertexBufferLayouts = fullVertexBuffers;
877
- const shaderModule = this.device.createShaderModule({
878
- label: "default model shader",
879
- code: DEFAULT_SHADER_WGSL,
880
- });
881
- const faceShaderModule = this.device.createShaderModule({
882
- label: "face NPR shader",
883
- code: FACE_SHADER_WGSL,
884
- });
885
- const hairShaderModule = this.device.createShaderModule({
886
- label: "hair NPR shader",
887
- code: HAIR_SHADER_WGSL,
888
- });
889
- const clothSmoothShaderModule = this.device.createShaderModule({
890
- label: "cloth smooth NPR shader",
891
- code: CLOTH_SMOOTH_SHADER_WGSL,
892
- });
893
- const clothRoughShaderModule = this.device.createShaderModule({
894
- label: "cloth rough NPR shader",
895
- code: CLOTH_ROUGH_SHADER_WGSL,
896
- });
897
- const metalShaderModule = this.device.createShaderModule({
898
- label: "metal NPR shader",
899
- code: METAL_SHADER_WGSL,
900
- });
901
- const bodyShaderModule = this.device.createShaderModule({
902
- label: "body NPR shader",
903
- code: BODY_SHADER_WGSL,
904
- });
905
- const eyeShaderModule = this.device.createShaderModule({
906
- label: "eye shader",
907
- code: EYE_SHADER_WGSL,
908
- });
909
- const stockingsShaderModule = this.device.createShaderModule({
910
- label: "stockings NPR shader",
911
- code: STOCKINGS_SHADER_WGSL,
912
- });
913
- const mmdClassicShaderModule = this.device.createShaderModule({
914
- label: "mmd classic shader",
915
- code: MMD_CLASSIC_SHADER_WGSL,
916
- });
917
868
  // group 0: per-frame (camera + light + sampler + shadow) — bound once per pass
918
869
  this.mainPerFrameBindGroupLayout = this.device.createBindGroupLayout({
919
870
  label: "main per-frame bind group layout",
@@ -941,7 +892,8 @@ export class Engine {
941
892
  ],
942
893
  });
943
894
  // group 2: per-material (textures + material uniforms) — bound per draw call.
944
- // Toon + sphere slots feed mmd_classic; other presets bind fallbacks.
895
+ // Toon + sphere texture slots (bindings 2/3) are reserved for future sphere/toon graph
896
+ // nodes; graphs that don't read them just bind the 1×1 white fallback.
945
897
  this.mainPerMaterialBindGroupLayout = this.device.createBindGroupLayout({
946
898
  label: "main per-material bind group layout",
947
899
  entries: [
@@ -971,179 +923,18 @@ export class Engine {
971
923
  });
972
924
  this.mainPipelineLayout = mainPipelineLayout;
973
925
  // perFrameBindGroup is created after shadow resources below
974
- this.modelPipeline = this.createRenderPipeline({
975
- label: "model pipeline",
976
- layout: mainPipelineLayout,
977
- shaderModule,
978
- vertexBuffers: fullVertexBuffers,
979
- fragmentTargets: sceneTargets,
980
- cullMode: "none",
981
- depthStencil: {
982
- format: "depth24plus-stencil8",
983
- depthWriteEnabled: true,
984
- depthCompare: "less-equal",
985
- },
986
- });
987
- this.facePipeline = this.createRenderPipeline({
988
- label: "face NPR pipeline",
989
- layout: mainPipelineLayout,
990
- shaderModule: faceShaderModule,
991
- vertexBuffers: fullVertexBuffers,
992
- fragmentTargets: sceneTargets,
993
- cullMode: "none",
994
- depthStencil: {
995
- format: "depth24plus-stencil8",
996
- depthWriteEnabled: true,
997
- depthCompare: "less-equal",
998
- },
999
- });
1000
- // Hair opaque: stencil != EYE_VALUE so fragments on top of eyes are skipped entirely —
1001
- // depth and color stay as the eye wrote them; the follow-up hairOverEyesPipeline then
1002
- // draws those skipped fragments alpha-blended so the eye reads through the hair.
1003
- this.hairPipeline = this.createRenderPipeline({
1004
- label: "hair NPR pipeline",
1005
- layout: mainPipelineLayout,
1006
- shaderModule: hairShaderModule,
1007
- vertexBuffers: fullVertexBuffers,
1008
- fragmentTargets: sceneTargets,
1009
- cullMode: "none",
1010
- depthStencil: {
1011
- format: "depth24plus-stencil8",
1012
- depthWriteEnabled: true,
1013
- depthCompare: "less-equal",
1014
- stencilFront: { compare: "not-equal", failOp: "keep", depthFailOp: "keep", passOp: "keep" },
1015
- stencilBack: { compare: "not-equal", failOp: "keep", depthFailOp: "keep", passOp: "keep" },
1016
- stencilReadMask: 0xff,
1017
- stencilWriteMask: 0,
1018
- },
1019
- });
1020
- // Eye-over-hair (the see-through-hair effect, inverted): after hair draws
1021
- // normally, the eye re-draws at 75% alpha with its depth pushed
1022
- // EYE_SEE_THROUGH_RANGE world units toward the camera, so it only shows
1023
- // through occluders within that window (bangs), never through the far
1024
- // side of the head. Stencil == EYE_VALUE confines it to pixels where the
1025
- // eye's primary draw passed depth — a hand in front of the face never
1026
- // stamps, so it still occludes fully.
1027
- // Hair-over-eyes: same shader with IS_OVER_EYES=true so alpha is scaled to 25% at compile time.
1028
- // Only fragments where eye stencil == EYE_VALUE pass; depth test still culls fragments
1029
- // that are further from camera than the eye, so hair behind the eye never shows through.
1030
- // depthWriteEnabled=false keeps the eye's depth authoritative for anything drawn after.
1031
- this.hairOverEyesPipeline = this.device.createRenderPipeline({
1032
- label: "hair over eyes pipeline",
1033
- layout: mainPipelineLayout,
1034
- vertex: { module: hairShaderModule, buffers: fullVertexBuffers },
1035
- fragment: {
1036
- module: hairShaderModule,
1037
- constants: { IS_OVER_EYES: 1 },
1038
- targets: sceneTargets,
1039
- },
1040
- primitive: { cullMode: "none" },
1041
- depthStencil: {
1042
- format: "depth24plus-stencil8",
1043
- depthWriteEnabled: false,
1044
- depthCompare: "less-equal",
1045
- stencilFront: { compare: "equal", failOp: "keep", depthFailOp: "keep", passOp: "keep" },
1046
- stencilBack: { compare: "equal", failOp: "keep", depthFailOp: "keep", passOp: "keep" },
1047
- stencilReadMask: 0xff,
1048
- stencilWriteMask: 0,
1049
- },
1050
- multisample: { count: Engine.MULTISAMPLE_COUNT },
1051
- });
1052
- this.clothSmoothPipeline = this.createRenderPipeline({
1053
- label: "cloth smooth NPR pipeline",
926
+ // Ungrouped materials render this neutral base — the compiled DEFAULT_GRAPH (diffuse
927
+ // texture x material color -> Principled BSDF). Grouped materials use their group's
928
+ // compiled pipeline instead. This is the single base shading model; the per-preset
929
+ // hand shaders are retired in favor of graphs.
930
+ const neutral = compileGraph(DEFAULT_GRAPH, { renderClass: "auto", alphaMode: "opaque" });
931
+ if (!neutral.ok)
932
+ throw new Error("failed to compile the neutral default graph");
933
+ const neutralModule = this.device.createShaderModule({ label: "neutral base (default graph)", code: neutral.wgsl });
934
+ this.neutralPipeline = this.createRenderPipeline({
935
+ label: "neutral base pipeline",
1054
936
  layout: mainPipelineLayout,
1055
- shaderModule: clothSmoothShaderModule,
1056
- vertexBuffers: fullVertexBuffers,
1057
- fragmentTargets: sceneTargets,
1058
- cullMode: "none",
1059
- depthStencil: {
1060
- format: "depth24plus-stencil8",
1061
- depthWriteEnabled: true,
1062
- depthCompare: "less-equal",
1063
- },
1064
- });
1065
- this.mmdClassicPipeline = this.createRenderPipeline({
1066
- label: "mmd classic pipeline",
1067
- layout: mainPipelineLayout,
1068
- shaderModule: mmdClassicShaderModule,
1069
- vertexBuffers: fullVertexBuffers,
1070
- fragmentTargets: sceneTargets,
1071
- cullMode: "none",
1072
- depthStencil: {
1073
- format: "depth24plus-stencil8",
1074
- depthWriteEnabled: true,
1075
- depthCompare: "less-equal",
1076
- },
1077
- });
1078
- this.clothRoughPipeline = this.createRenderPipeline({
1079
- label: "cloth rough NPR pipeline",
1080
- layout: mainPipelineLayout,
1081
- shaderModule: clothRoughShaderModule,
1082
- vertexBuffers: fullVertexBuffers,
1083
- fragmentTargets: sceneTargets,
1084
- cullMode: "none",
1085
- depthStencil: {
1086
- format: "depth24plus-stencil8",
1087
- depthWriteEnabled: true,
1088
- depthCompare: "less-equal",
1089
- },
1090
- });
1091
- this.metalPipeline = this.createRenderPipeline({
1092
- label: "metal NPR pipeline",
1093
- layout: mainPipelineLayout,
1094
- shaderModule: metalShaderModule,
1095
- vertexBuffers: fullVertexBuffers,
1096
- fragmentTargets: sceneTargets,
1097
- cullMode: "none",
1098
- depthStencil: {
1099
- format: "depth24plus-stencil8",
1100
- depthWriteEnabled: true,
1101
- depthCompare: "less-equal",
1102
- },
1103
- });
1104
- this.bodyPipeline = this.createRenderPipeline({
1105
- label: "body NPR pipeline",
1106
- layout: mainPipelineLayout,
1107
- shaderModule: bodyShaderModule,
1108
- vertexBuffers: fullVertexBuffers,
1109
- fragmentTargets: sceneTargets,
1110
- cullMode: "none",
1111
- depthStencil: {
1112
- format: "depth24plus-stencil8",
1113
- depthWriteEnabled: true,
1114
- depthCompare: "less-equal",
1115
- },
1116
- });
1117
- // Eye: stamps stencil = EYE_VALUE on every fragment it writes. Later hair passes read
1118
- // this stamp to split into "draw normally (not over eye)" vs "draw alpha-blended".
1119
- // cullMode="front" + small negative depthBias is the MMD post-alpha-eye trick: only the
1120
- // back half of the eye sphere renders, it passes depth against the face (via bias) when
1121
- // viewed from the front, and it gets culled when viewed from behind — so eye fragments
1122
- // can't leak through the back of the head without needing a per-model skull occluder.
1123
- this.eyePipeline = this.createRenderPipeline({
1124
- label: "eye pipeline",
1125
- layout: mainPipelineLayout,
1126
- shaderModule: eyeShaderModule,
1127
- vertexBuffers: fullVertexBuffers,
1128
- fragmentTargets: sceneTargets,
1129
- cullMode: "front",
1130
- depthStencil: {
1131
- format: "depth24plus-stencil8",
1132
- depthWriteEnabled: true,
1133
- depthCompare: "less-equal",
1134
- depthBias: -0.00005,
1135
- depthBiasSlopeScale: 0.0,
1136
- depthBiasClamp: 0.0,
1137
- stencilFront: { compare: "always", failOp: "keep", depthFailOp: "keep", passOp: "replace" },
1138
- stencilBack: { compare: "always", failOp: "keep", depthFailOp: "keep", passOp: "replace" },
1139
- stencilReadMask: 0xff,
1140
- stencilWriteMask: 0xff,
1141
- },
1142
- });
1143
- this.stockingsPipeline = this.createRenderPipeline({
1144
- label: "stockings NPR pipeline",
1145
- layout: mainPipelineLayout,
1146
- shaderModule: stockingsShaderModule,
937
+ shaderModule: neutralModule,
1147
938
  vertexBuffers: fullVertexBuffers,
1148
939
  fragmentTargets: sceneTargets,
1149
940
  cullMode: "none",
@@ -2139,7 +1930,6 @@ export class Engine {
2139
1930
  firstIndex: 0,
2140
1931
  bindGroup: this.groundShadowBindGroup,
2141
1932
  materialName: "Ground",
2142
- preset: "cloth_rough",
2143
1933
  groupId: null,
2144
1934
  };
2145
1935
  }
@@ -2289,17 +2079,6 @@ export class Engine {
2289
2079
  const boneIndex = inst.model.getSkeleton().bones.findIndex((b) => b.name === boneName);
2290
2080
  this.selectedBone = boneIndex >= 0 ? { modelName, boneName, boneIndex } : null;
2291
2081
  }
2292
- setMaterialPresets(modelName, presets) {
2293
- const inst = this.modelInstances.get(modelName);
2294
- if (!inst)
2295
- return;
2296
- inst.materialPresets = presets;
2297
- // Only updates the ungrouped fallback preset; grouped materials render via their
2298
- // group regardless. binding(4) is unaffected (ungrouped always binds the zero
2299
- // buffer; grouped bindings are owned by applyStyleGroups).
2300
- for (const dc of inst.drawCalls)
2301
- dc.preset = resolvePreset(dc.materialName, presets);
2302
- }
2303
2082
  // Build a material's bind group with binding(4) pointing at a given StyleUniforms buffer
2304
2083
  // (the group's buffer when grouped, or the shared zero buffer when ungrouped).
2305
2084
  createMaterialBindGroup(label, baseEntries, styleBuffer) {
@@ -2513,7 +2292,6 @@ export class Engine {
2513
2292
  pickPerInstanceBindGroup,
2514
2293
  pickDrawCalls: [],
2515
2294
  hiddenMaterials: new Set(),
2516
- materialPresets: undefined,
2517
2295
  physics,
2518
2296
  vertexBufferNeedsUpdate: false,
2519
2297
  gpuMorph,
@@ -2752,7 +2530,6 @@ export class Engine {
2752
2530
  }
2753
2531
  const materialUniformBuffer = this.createMaterialUniformBuffer(prefix + mat.name, mat, sphereMode, headBoneIndex);
2754
2532
  inst.gpuBuffers.push(materialUniformBuffer);
2755
- const preset = resolvePreset(mat.name, inst.materialPresets);
2756
2533
  const textureView = diffuseTexture.createView();
2757
2534
  const baseBindGroupEntries = [
2758
2535
  { binding: 0, resource: textureView },
@@ -2760,7 +2537,8 @@ export class Engine {
2760
2537
  { binding: 2, resource: (toonTexture ?? this.fallbackMaterialTexture).createView() },
2761
2538
  { binding: 3, resource: (sphereTexture ?? this.fallbackMaterialTexture).createView() },
2762
2539
  ];
2763
- // Ungrouped at load — binding(4) = zero buffer. applyStyleGroups rebinds grouped ones.
2540
+ // Ungrouped at load — binding(4) = zero buffer, neutral base pipeline. autoStyleGroups
2541
+ // / applyStyleGroups rebind grouped materials to their group's buffer + pipeline.
2764
2542
  const bindGroup = this.createMaterialBindGroup(`${prefix}material: ${mat.name}`, baseBindGroupEntries, this.zeroStyleBuffer);
2765
2543
  const type = isTransparent ? "transparent" : "opaque";
2766
2544
  inst.drawCalls.push({
@@ -2769,7 +2547,6 @@ export class Engine {
2769
2547
  firstIndex: currentIndexOffset,
2770
2548
  bindGroup,
2771
2549
  materialName: mat.name,
2772
- preset,
2773
2550
  groupId: null,
2774
2551
  baseBindGroupEntries,
2775
2552
  });
@@ -2798,7 +2575,6 @@ export class Engine {
2798
2575
  firstIndex: currentIndexOffset,
2799
2576
  bindGroup: outlineBindGroup,
2800
2577
  materialName: mat.name,
2801
- preset,
2802
2578
  groupId: null,
2803
2579
  });
2804
2580
  }
@@ -2861,33 +2637,66 @@ export class Engine {
2861
2637
  if (cached) {
2862
2638
  return cached;
2863
2639
  }
2640
+ let buffer;
2864
2641
  try {
2865
- const buffer = await inst.assetReader.readBinary(logicalPath);
2866
- const imageBitmap = await createImageBitmap(new Blob([buffer]), {
2867
- premultiplyAlpha: "none",
2868
- colorSpaceConversion: "none",
2869
- });
2870
- const mipLevelCount = Math.floor(Math.log2(Math.max(imageBitmap.width, imageBitmap.height))) + 1;
2871
- const texture = this.device.createTexture({
2872
- label: `texture: ${cacheKey}`,
2873
- size: [imageBitmap.width, imageBitmap.height],
2874
- format: "rgba8unorm-srgb",
2875
- mipLevelCount,
2876
- usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT,
2877
- });
2878
- this.device.queue.copyExternalImageToTexture({ source: imageBitmap }, { texture }, [
2879
- imageBitmap.width,
2880
- imageBitmap.height,
2881
- ]);
2882
- if (mipLevelCount > 1)
2883
- this.generateMipmaps(texture, mipLevelCount);
2884
- this.textureCache.set(cacheKey, texture);
2885
- inst.textureCacheKeys.push(cacheKey);
2886
- return texture;
2887
- }
2888
- catch {
2642
+ buffer = await inst.assetReader.readBinary(logicalPath);
2643
+ }
2644
+ catch (e) {
2645
+ console.warn(`[reze] texture read failed: ${logicalPath}`, e instanceof Error ? e.message : e);
2889
2646
  return null;
2890
2647
  }
2648
+ // Decode to either an ImageBitmap (web-native formats) or raw RGBA (TGA). .tga skips
2649
+ // straight to the TGA decoder (createImageBitmap can't read it); other extensions try
2650
+ // the browser decoder first, then fall back to TGA in case a .spa/.sph/etc. is TGA
2651
+ // under its extension. Every failure is logged and soft — never throws to the caller.
2652
+ let source = null;
2653
+ let rgba = null;
2654
+ let width;
2655
+ let height;
2656
+ const isTga = logicalPath.toLowerCase().endsWith(".tga");
2657
+ if (!isTga) {
2658
+ try {
2659
+ source = await createImageBitmap(new Blob([buffer]), { premultiplyAlpha: "none", colorSpaceConversion: "none" });
2660
+ }
2661
+ catch {
2662
+ source = null; // not a browser-native image — try TGA below
2663
+ }
2664
+ }
2665
+ if (source) {
2666
+ width = source.width;
2667
+ height = source.height;
2668
+ }
2669
+ else {
2670
+ try {
2671
+ const img = decodeTga(buffer);
2672
+ rgba = img.rgba;
2673
+ width = img.width;
2674
+ height = img.height;
2675
+ }
2676
+ catch (e) {
2677
+ console.warn(`[reze] texture decode failed (unsupported format?): ${logicalPath}`, e instanceof Error ? e.message : e);
2678
+ return null;
2679
+ }
2680
+ }
2681
+ const mipLevelCount = Math.floor(Math.log2(Math.max(width, height))) + 1;
2682
+ const texture = this.device.createTexture({
2683
+ label: `texture: ${cacheKey}`,
2684
+ size: [width, height],
2685
+ format: "rgba8unorm-srgb",
2686
+ mipLevelCount,
2687
+ usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT,
2688
+ });
2689
+ if (source) {
2690
+ this.device.queue.copyExternalImageToTexture({ source }, { texture }, [width, height]);
2691
+ }
2692
+ else {
2693
+ this.device.queue.writeTexture({ texture }, rgba, { bytesPerRow: width * 4, rowsPerImage: height }, [width, height]);
2694
+ }
2695
+ if (mipLevelCount > 1)
2696
+ this.generateMipmaps(texture, mipLevelCount);
2697
+ this.textureCache.set(cacheKey, texture);
2698
+ inst.textureCacheKeys.push(cacheKey);
2699
+ return texture;
2891
2700
  }
2892
2701
  // Bilinear box-filter downsample per level. Reads srgb view (hardware linearizes on sample,
2893
2702
  // re-encodes on write), so intensities are filtered in linear space — matching EEVEE/Blender.
@@ -3405,15 +3214,14 @@ export class Engine {
3405
3214
  return [...inst.styleGroups.values()].map((g) => g.group);
3406
3215
  }
3407
3216
  /**
3408
- * Create default style groups from the model's material→preset resolution
3409
- * (`setMaterialPresets` map first, then name hints), so a freshly loaded model comes up
3410
- * editably grouped with zero interaction. Materials resolving to `mmd_classic` (no map
3411
- * entry, no hint) stay ungrouped. The returned promise resolves after grouping AND the
3412
- * async graph compiles, so `getStyleGroups` is populated the moment it resolves. Opt-in:
3413
- * call it explicitly (typically after `setMaterialPresets`); a bare load stays ungrouped
3414
- * / byte-identical to before.
3217
+ * Create default style groups from each material's resolved style category — `overrides`
3218
+ * (material name → category) first, then the built-in JP/CN/EN name hints. So a
3219
+ * standard-named model auto-groups with no overrides; a custom-named one passes a map for
3220
+ * the materials the hints miss. Unmatched materials (no override, no hint) stay ungrouped
3221
+ * (neutral default). The category picks the default graph + render-class + alpha-mode.
3222
+ * Resolves after grouping AND the async compiles, so `getStyleGroups` is then populated.
3415
3223
  */
3416
- async autoStyleGroups(modelName) {
3224
+ async autoStyleGroups(modelName, overrides) {
3417
3225
  const inst = this.modelInstances.get(modelName);
3418
3226
  if (!inst)
3419
3227
  return { ok: false, groups: [], unknownMaterials: [], conflicts: [] };
@@ -3421,7 +3229,7 @@ export class Engine {
3421
3229
  for (const dc of inst.drawCalls) {
3422
3230
  if (!dc.baseBindGroupEntries)
3423
3231
  continue; // material draw calls only (skip outlines)
3424
- const preset = resolvePreset(dc.materialName, inst.materialPresets);
3232
+ const preset = resolvePreset(dc.materialName, overrides);
3425
3233
  if (preset === "mmd_classic")
3426
3234
  continue; // unmatched → ungrouped
3427
3235
  const arr = buckets.get(preset) ?? [];
@@ -3653,13 +3461,11 @@ export class Engine {
3653
3461
  }
3654
3462
  this.device.queue.writeBuffer(buffer, 0, data);
3655
3463
  }
3656
- // Draw-order rank within a bucket: eye stamps before hair reads. A grouped call ranks by
3657
- // its group's render-class; an ungrouped call by its preset (the hand-shader path).
3464
+ // Draw-order rank within a bucket: eye stamps before hair reads. Purely from the group's
3465
+ // render-class ungrouped materials are neutral (rank 0, no stencil interplay).
3658
3466
  drawCallRank(inst, dc) {
3659
- const rc = dc.groupId ? (inst.styleGroups.get(dc.groupId)?.renderClass ?? "auto") : undefined;
3660
- if (rc)
3661
- return rc === "hair" ? 2 : rc === "eye" ? 1 : 0;
3662
- return dc.preset === "hair" ? 2 : dc.preset === "eye" ? 1 : 0;
3467
+ const rc = dc.groupId ? (inst.styleGroups.get(dc.groupId)?.renderClass ?? "auto") : "auto";
3468
+ return rc === "hair" ? 2 : rc === "eye" ? 1 : 0;
3663
3469
  }
3664
3470
  sortDrawCalls(inst) {
3665
3471
  const typeOrder = {
@@ -3732,35 +3538,14 @@ export class Engine {
3732
3538
  });
3733
3539
  }
3734
3540
  // Pipeline for a material draw call: its group's compiled pipeline when grouped, else
3735
- // the built-in preset (hand-shader) pipeline.
3541
+ // the neutral base (ungrouped materials render the default graph).
3736
3542
  pipelineForDrawCall(inst, dc) {
3737
3543
  if (dc.groupId) {
3738
3544
  const install = inst.styleGroups.get(dc.groupId);
3739
3545
  if (install)
3740
3546
  return install.pipeline;
3741
3547
  }
3742
- return this.pipelineForPreset(dc.preset);
3743
- }
3744
- pipelineForPreset(preset) {
3745
- if (preset === "face")
3746
- return this.facePipeline;
3747
- if (preset === "hair")
3748
- return this.hairPipeline;
3749
- if (preset === "cloth_smooth")
3750
- return this.clothSmoothPipeline;
3751
- if (preset === "cloth_rough")
3752
- return this.clothRoughPipeline;
3753
- if (preset === "metal")
3754
- return this.metalPipeline;
3755
- if (preset === "body")
3756
- return this.bodyPipeline;
3757
- if (preset === "eye")
3758
- return this.eyePipeline;
3759
- if (preset === "stockings")
3760
- return this.stockingsPipeline;
3761
- if (preset === "mmd_classic")
3762
- return this.mmdClassicPipeline;
3763
- return this.modelPipeline;
3548
+ return this.neutralPipeline;
3764
3549
  }
3765
3550
  /**
3766
3551
  * Draw every material of a given type (`opaque` or `transparent`) using the main
@@ -3830,10 +3615,10 @@ export class Engine {
3830
3615
  this.drawOutlines(pass, inst, "transparent-outline");
3831
3616
  }
3832
3617
  /**
3833
- * Second hair pass for the see-through-hair effect. Re-draws every hair-class opaque
3834
- * draw with its over-eyes pipeline — stencil-matched to `EYE_VALUE`, `IS_OVER_EYES=true`
3835
- * (25% alpha), depth-write off. Covers both grouped hair-class draws (their compiled
3836
- * over-eyes pipeline) and ungrouped hair-preset draws (the hand `hairOverEyesPipeline`).
3618
+ * Second hair pass for the see-through-hair effect. Re-draws every hair-class grouped
3619
+ * opaque draw with its compiled over-eyes pipeline — stencil-matched to `EYE_VALUE`,
3620
+ * `IS_OVER_EYES=true` (25% alpha), depth-write off. Ungrouped materials are neutral and
3621
+ * never participate.
3837
3622
  */
3838
3623
  drawHairOverEyes(pass, inst) {
3839
3624
  let bound = false;
@@ -3857,13 +3642,13 @@ export class Engine {
3857
3642
  pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
3858
3643
  }
3859
3644
  }
3860
- // The over-eyes pipeline for a hair draw call, or null if it isn't hair-class.
3645
+ // The over-eyes pipeline for a hair-class grouped draw call, or null. Ungrouped
3646
+ // materials are neutral — no see-through pass.
3861
3647
  overEyesPipelineFor(inst, dc) {
3862
- if (dc.groupId) {
3863
- const install = inst.styleGroups.get(dc.groupId);
3864
- return install?.renderClass === "hair" ? (install.overEyesPipeline ?? null) : null;
3865
- }
3866
- return dc.preset === "hair" ? this.hairOverEyesPipeline : null;
3648
+ if (!dc.groupId)
3649
+ return null;
3650
+ const install = inst.styleGroups.get(dc.groupId);
3651
+ return install?.renderClass === "hair" ? (install.overEyesPipeline ?? null) : null;
3867
3652
  }
3868
3653
  updateCameraUniforms() {
3869
3654
  const viewMatrix = this.camera.getViewMatrix();
@@ -1,4 +1,4 @@
1
- import type { Diagnostic, ExposedParam, StyleGraph } from "./schema";
1
+ import type { Diagnostic, ExposedParam, ShaderGraph } from "./schema";
2
2
  import type { AlphaMode, RenderClass } from "./render-class";
3
3
  export type CompileOptions = {
4
4
  /** Fold exposed params to their defaults as consts (no StyleUniforms binding).
@@ -35,6 +35,6 @@ export type CompileResult = {
35
35
  prunedNodes: string[];
36
36
  };
37
37
  export declare function assignStyleSlots(params: ExposedParam[]): StyleSlot[];
38
- export declare function validateGraph(graph: StyleGraph, opts?: CompileOptions): Diagnostic[];
39
- export declare function compileGraph(graph: StyleGraph, opts?: CompileOptions): CompileResult;
38
+ export declare function validateGraph(graph: ShaderGraph, opts?: CompileOptions): Diagnostic[];
39
+ export declare function compileGraph(graph: ShaderGraph, opts?: CompileOptions): CompileResult;
40
40
  //# sourceMappingURL=compile.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"compile.d.ts","sourceRoot":"","sources":["../../src/graph/compile.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,UAAU,EAAE,YAAY,EAA0B,UAAU,EAAE,MAAM,UAAU,CAAA;AAI5F,OAAO,KAAK,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAA;AAE5D,MAAM,MAAM,cAAc,GAAG;IAC3B;8EAC0E;IAC1E,YAAY,CAAC,EAAE,OAAO,CAAA;IACtB;6DACyD;IACzD,WAAW,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAA;IAC9C;0EACsE;IACtE,WAAW,CAAC,EAAE,WAAW,CAAA;IACzB,wEAAwE;IACxE,SAAS,CAAC,EAAE,SAAS,CAAA;CACtB,CAAA;AAED,yFAAyF;AACzF,MAAM,MAAM,SAAS,GAAG;IACtB,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,OAAO,GAAG,OAAO,CAAA;IACvB,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,CAAC,EAAE,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,CAAA;IACjC,IAAI,EAAE,MAAM,CAAA;CACb,CAAA;AAED,MAAM,MAAM,aAAa,GAAG;IAC1B,EAAE,EAAE,OAAO,CAAA;IACX,gDAAgD;IAChD,IAAI,EAAE,MAAM,CAAA;IACZ,6EAA6E;IAC7E,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,EAAE,SAAS,EAAE,CAAA;IACpB,WAAW,EAAE,UAAU,EAAE,CAAA;IACzB,WAAW,EAAE,MAAM,EAAE,CAAA;CACtB,CAAA;AAcD,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,YAAY,EAAE,GAAG,SAAS,EAAE,CAuBpE;AAID,wBAAgB,aAAa,CAAC,KAAK,EAAE,UAAU,EAAE,IAAI,GAAE,cAAmB,GAAG,UAAU,EAAE,CAqGxF;AAID,wBAAgB,YAAY,CAAC,KAAK,EAAE,UAAU,EAAE,IAAI,GAAE,cAAmB,GAAG,aAAa,CA6JxF"}
1
+ {"version":3,"file":"compile.d.ts","sourceRoot":"","sources":["../../src/graph/compile.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,UAAU,EAAE,YAAY,EAA0B,WAAW,EAAE,MAAM,UAAU,CAAA;AAI7F,OAAO,KAAK,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAA;AAE5D,MAAM,MAAM,cAAc,GAAG;IAC3B;8EAC0E;IAC1E,YAAY,CAAC,EAAE,OAAO,CAAA;IACtB;6DACyD;IACzD,WAAW,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAA;IAC9C;0EACsE;IACtE,WAAW,CAAC,EAAE,WAAW,CAAA;IACzB,wEAAwE;IACxE,SAAS,CAAC,EAAE,SAAS,CAAA;CACtB,CAAA;AAED,yFAAyF;AACzF,MAAM,MAAM,SAAS,GAAG;IACtB,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,OAAO,GAAG,OAAO,CAAA;IACvB,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,CAAC,EAAE,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,CAAA;IACjC,IAAI,EAAE,MAAM,CAAA;CACb,CAAA;AAED,MAAM,MAAM,aAAa,GAAG;IAC1B,EAAE,EAAE,OAAO,CAAA;IACX,gDAAgD;IAChD,IAAI,EAAE,MAAM,CAAA;IACZ,6EAA6E;IAC7E,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,EAAE,SAAS,EAAE,CAAA;IACpB,WAAW,EAAE,UAAU,EAAE,CAAA;IACzB,WAAW,EAAE,MAAM,EAAE,CAAA;CACtB,CAAA;AAcD,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,YAAY,EAAE,GAAG,SAAS,EAAE,CAuBpE;AAID,wBAAgB,aAAa,CAAC,KAAK,EAAE,WAAW,EAAE,IAAI,GAAE,cAAmB,GAAG,UAAU,EAAE,CAqGzF;AAID,wBAAgB,YAAY,CAAC,KAAK,EAAE,WAAW,EAAE,IAAI,GAAE,cAAmB,GAAG,aAAa,CA6JzF"}
@@ -1,3 +1,3 @@
1
- import type { StyleGraph } from "../schema";
2
- export declare const BODY_GRAPH: StyleGraph;
1
+ import type { ShaderGraph } from "../schema";
2
+ export declare const BODY_GRAPH: ShaderGraph;
3
3
  //# sourceMappingURL=body.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"body.d.ts","sourceRoot":"","sources":["../../../src/graph/presets/body.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,WAAW,CAAA;AAE3C,eAAO,MAAM,UAAU,EAAE,UA+FxB,CAAA"}
1
+ {"version":3,"file":"body.d.ts","sourceRoot":"","sources":["../../../src/graph/presets/body.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,WAAW,CAAA;AAE5C,eAAO,MAAM,UAAU,EAAE,WA+FxB,CAAA"}
@@ -1,4 +1,4 @@
1
- // M_Body as a StyleGraph — port of shaders/materials/body.ts (仿深空之眼 "M_Body").
1
+ // M_Body as a ShaderGraph — port of shaders/materials/body.ts (仿深空之眼 "M_Body").
2
2
  // Toon + warm rim + rim1/rim2 stack mixed 50/50 against a Principled BSDF with
3
3
  // noise-bumped normal. The hand port's local ramp_ease is ramp_cardinal (identical
4
4
  // smoothstep form); the Mapping node's zero loc/rot folds to a scale multiply.
@@ -1,3 +1,3 @@
1
- import type { StyleGraph } from "../schema";
2
- export declare const CLOTH_ROUGH_GRAPH: StyleGraph;
1
+ import type { ShaderGraph } from "../schema";
2
+ export declare const CLOTH_ROUGH_GRAPH: ShaderGraph;
3
3
  //# sourceMappingURL=cloth_rough.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"cloth_rough.d.ts","sourceRoot":"","sources":["../../../src/graph/presets/cloth_rough.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,WAAW,CAAA;AAE3C,eAAO,MAAM,iBAAiB,EAAE,UAwD/B,CAAA"}
1
+ {"version":3,"file":"cloth_rough.d.ts","sourceRoot":"","sources":["../../../src/graph/presets/cloth_rough.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,WAAW,CAAA;AAE5C,eAAO,MAAM,iBAAiB,EAAE,WAwD/B,CAAA"}
@@ -1,4 +1,4 @@
1
- // M_Rough_Cloth as a StyleGraph — port of shaders/materials/cloth_rough.ts.
1
+ // M_Rough_Cloth as a ShaderGraph — port of shaders/materials/cloth_rough.ts.
2
2
  // NPR graph identical to M_Smooth_Cloth, but the noise bump subtree IS live on
3
3
  // Principled.Normal (weave bump in rest space) and Roughness is raised to 0.8187.
4
4
  // The tex_noise node hits the detail=2 peephole → tex_noise_d2.
@@ -1,3 +1,3 @@
1
- import type { StyleGraph } from "../schema";
2
- export declare const CLOTH_SMOOTH_GRAPH: StyleGraph;
1
+ import type { ShaderGraph } from "../schema";
2
+ export declare const CLOTH_SMOOTH_GRAPH: ShaderGraph;
3
3
  //# sourceMappingURL=cloth_smooth.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"cloth_smooth.d.ts","sourceRoot":"","sources":["../../../src/graph/presets/cloth_smooth.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,WAAW,CAAA;AAE3C,eAAO,MAAM,kBAAkB,EAAE,UAgDhC,CAAA"}
1
+ {"version":3,"file":"cloth_smooth.d.ts","sourceRoot":"","sources":["../../../src/graph/presets/cloth_smooth.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,WAAW,CAAA;AAE5C,eAAO,MAAM,kBAAkB,EAAE,WAgDhC,CAAA"}
@@ -1,4 +1,4 @@
1
- // M_Smooth_Cloth as a StyleGraph — port of shaders/materials/cloth_smooth.ts.
1
+ // M_Smooth_Cloth as a ShaderGraph — port of shaders/materials/cloth_smooth.ts.
2
2
  // NPR toon + bevel + overlay-boosted emission (18.2×) mixed 10/90 against a plain
3
3
  // Principled BSDF. The Blender graph's dead bump subtree is omitted (as in the hand
4
4
  // port). hue_sat nodes with hue=0.5 compile to the hue_sat_id specialization.
@@ -1,3 +1,3 @@
1
- import type { StyleGraph } from "../schema";
2
- export declare const DEFAULT_GRAPH: StyleGraph;
1
+ import type { ShaderGraph } from "../schema";
2
+ export declare const DEFAULT_GRAPH: ShaderGraph;
3
3
  //# sourceMappingURL=default.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"default.d.ts","sourceRoot":"","sources":["../../../src/graph/presets/default.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,WAAW,CAAA;AAE3C,eAAO,MAAM,aAAa,EAAE,UAc3B,CAAA"}
1
+ {"version":3,"file":"default.d.ts","sourceRoot":"","sources":["../../../src/graph/presets/default.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,WAAW,CAAA;AAE5C,eAAO,MAAM,aAAa,EAAE,WAoB3B,CAAA"}