reze-engine 0.1.6 → 0.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/engine.js CHANGED
@@ -10,6 +10,9 @@ export class Engine {
10
10
  this.vertexCount = 0;
11
11
  this.resizeObserver = null;
12
12
  this.sampleCount = 4; // MSAA 4x
13
+ // Bloom settings
14
+ this.bloomThreshold = 0.3;
15
+ this.bloomIntensity = 0.14;
13
16
  this.currentModel = null;
14
17
  this.modelDir = "";
15
18
  this.physics = null;
@@ -60,6 +63,8 @@ export class Engine {
60
63
  this.setupCamera();
61
64
  this.setupLighting();
62
65
  this.createPipelines();
66
+ this.createFullscreenQuad();
67
+ this.createBloomPipelines();
63
68
  this.setupResize();
64
69
  }
65
70
  // Step 2: Create shaders and render pipelines
@@ -286,9 +291,8 @@ export class Engine {
286
291
  discard;
287
292
  }
288
293
 
289
- // For hair-over-eyes effect: simple half-transparent overlay
290
- // Use 60% opacity to create a semi-transparent hair color overlay
291
- let overlayAlpha = finalAlpha * 0.6;
294
+ // For hair-over-eyes effect: simple half-transparent overlay - Use 50% opacity to create a semi-transparent hair color overlay
295
+ let overlayAlpha = finalAlpha * 0.5;
292
296
 
293
297
  return vec4f(clamp(color, vec3f(0.0), vec3f(1.0)), overlayAlpha);
294
298
  }
@@ -519,8 +523,7 @@ export class Engine {
519
523
  count: this.sampleCount,
520
524
  },
521
525
  });
522
- // Hair outline pipeline: draws hair outlines over non-eyes (stencil != 1)
523
- // Drawn after hair geometry, so depth testing ensures outlines only appear where hair exists
526
+ // Hair outline pipeline: draws hair outlines over non-eyes (stencil != 1) - Drawn after hair geometry, so depth testing ensures outlines only appear where hair exists
524
527
  this.hairOutlinePipeline = this.device.createRenderPipeline({
525
528
  label: "hair outline pipeline",
526
529
  layout: outlinePipelineLayout,
@@ -601,8 +604,7 @@ export class Engine {
601
604
  count: this.sampleCount,
602
605
  },
603
606
  });
604
- // Hair outline pipeline for over eyes: draws where stencil == 1, but only where hair depth exists
605
- // Uses depth compare "equal" with a small bias to only appear where hair geometry exists
607
+ // Hair outline pipeline for over eyes: draws where stencil == 1, but only where hair depth exists - Uses depth compare "equal" with a small bias to only appear where hair geometry exists
606
608
  this.hairOutlineOverEyesPipeline = this.device.createRenderPipeline({
607
609
  label: "hair outline over eyes pipeline",
608
610
  layout: outlinePipelineLayout,
@@ -718,8 +720,7 @@ export class Engine {
718
720
  format: this.presentationFormat,
719
721
  blend: {
720
722
  color: {
721
- // Simple half-transparent overlay effect
722
- // Blend: hairColor * overlayAlpha + eyeColor * (1 - overlayAlpha)
723
+ // Simple half-transparent overlay effect - Blend: hairColor * overlayAlpha + eyeColor * (1 - overlayAlpha)
723
724
  srcFactor: "src-alpha",
724
725
  dstFactor: "one-minus-src-alpha",
725
726
  operation: "add",
@@ -923,6 +924,271 @@ export class Engine {
923
924
  },
924
925
  });
925
926
  }
927
+ // Create fullscreen quad for post-processing
928
+ createFullscreenQuad() {
929
+ // Fullscreen quad vertices: two triangles covering the entire screen - Format: position (x, y), uv (u, v)
930
+ const quadVertices = new Float32Array([
931
+ // Triangle 1
932
+ -1.0,
933
+ -1.0,
934
+ 0.0,
935
+ 0.0, // bottom-left
936
+ 1.0,
937
+ -1.0,
938
+ 1.0,
939
+ 0.0, // bottom-right
940
+ -1.0,
941
+ 1.0,
942
+ 0.0,
943
+ 1.0, // top-left
944
+ // Triangle 2
945
+ -1.0,
946
+ 1.0,
947
+ 0.0,
948
+ 1.0, // top-left
949
+ 1.0,
950
+ -1.0,
951
+ 1.0,
952
+ 0.0, // bottom-right
953
+ 1.0,
954
+ 1.0,
955
+ 1.0,
956
+ 1.0, // top-right
957
+ ]);
958
+ this.fullscreenQuadBuffer = this.device.createBuffer({
959
+ label: "fullscreen quad",
960
+ size: quadVertices.byteLength,
961
+ usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
962
+ });
963
+ this.device.queue.writeBuffer(this.fullscreenQuadBuffer, 0, quadVertices);
964
+ }
965
+ // Create bloom post-processing pipelines
966
+ createBloomPipelines() {
967
+ // Bloom extraction shader (extracts bright areas)
968
+ const bloomExtractShader = this.device.createShaderModule({
969
+ label: "bloom extract",
970
+ code: /* wgsl */ `
971
+ struct VertexOutput {
972
+ @builtin(position) position: vec4f,
973
+ @location(0) uv: vec2f,
974
+ };
975
+
976
+ @vertex fn vs(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput {
977
+ var output: VertexOutput;
978
+ // Generate fullscreen quad from vertex index
979
+ let x = f32((vertexIndex << 1u) & 2u) * 2.0 - 1.0;
980
+ let y = f32(vertexIndex & 2u) * 2.0 - 1.0;
981
+ output.position = vec4f(x, y, 0.0, 1.0);
982
+ output.uv = vec2f(x * 0.5 + 0.5, 1.0 - (y * 0.5 + 0.5));
983
+ return output;
984
+ }
985
+
986
+ struct BloomExtractUniforms {
987
+ threshold: f32,
988
+ _padding1: f32,
989
+ _padding2: f32,
990
+ _padding3: f32,
991
+ _padding4: f32,
992
+ _padding5: f32,
993
+ _padding6: f32,
994
+ _padding7: f32,
995
+ };
996
+
997
+ @group(0) @binding(0) var inputTexture: texture_2d<f32>;
998
+ @group(0) @binding(1) var inputSampler: sampler;
999
+ @group(0) @binding(2) var<uniform> extractUniforms: BloomExtractUniforms;
1000
+
1001
+ @fragment fn fs(input: VertexOutput) -> @location(0) vec4f {
1002
+ let color = textureSample(inputTexture, inputSampler, input.uv);
1003
+ // Extract bright areas above threshold
1004
+ let threshold = extractUniforms.threshold;
1005
+ let bloom = max(vec3f(0.0), color.rgb - vec3f(threshold)) / max(0.001, 1.0 - threshold);
1006
+ return vec4f(bloom, color.a);
1007
+ }
1008
+ `,
1009
+ });
1010
+ // Bloom blur shader (gaussian blur - can be used for both horizontal and vertical)
1011
+ const bloomBlurShader = this.device.createShaderModule({
1012
+ label: "bloom blur",
1013
+ code: /* wgsl */ `
1014
+ struct VertexOutput {
1015
+ @builtin(position) position: vec4f,
1016
+ @location(0) uv: vec2f,
1017
+ };
1018
+
1019
+ @vertex fn vs(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput {
1020
+ var output: VertexOutput;
1021
+ let x = f32((vertexIndex << 1u) & 2u) * 2.0 - 1.0;
1022
+ let y = f32(vertexIndex & 2u) * 2.0 - 1.0;
1023
+ output.position = vec4f(x, y, 0.0, 1.0);
1024
+ output.uv = vec2f(x * 0.5 + 0.5, 1.0 - (y * 0.5 + 0.5));
1025
+ return output;
1026
+ }
1027
+
1028
+ struct BlurUniforms {
1029
+ direction: vec2f,
1030
+ _padding1: f32,
1031
+ _padding2: f32,
1032
+ _padding3: f32,
1033
+ _padding4: f32,
1034
+ _padding5: f32,
1035
+ _padding6: f32,
1036
+ };
1037
+
1038
+ @group(0) @binding(0) var inputTexture: texture_2d<f32>;
1039
+ @group(0) @binding(1) var inputSampler: sampler;
1040
+ @group(0) @binding(2) var<uniform> blurUniforms: BlurUniforms;
1041
+
1042
+ // 9-tap gaussian blur
1043
+ @fragment fn fs(input: VertexOutput) -> @location(0) vec4f {
1044
+ let texelSize = 1.0 / vec2f(textureDimensions(inputTexture));
1045
+ var result = vec4f(0.0);
1046
+
1047
+ // Gaussian weights for 9-tap filter
1048
+ let weights = array<f32, 9>(
1049
+ 0.01621622, 0.05405405, 0.12162162,
1050
+ 0.19459459, 0.22702703,
1051
+ 0.19459459, 0.12162162, 0.05405405, 0.01621622
1052
+ );
1053
+
1054
+ let offsets = array<f32, 9>(-4.0, -3.0, -2.0, -1.0, 0.0, 1.0, 2.0, 3.0, 4.0);
1055
+
1056
+ for (var i = 0u; i < 9u; i++) {
1057
+ let offset = offsets[i] * texelSize * blurUniforms.direction;
1058
+ result += textureSample(inputTexture, inputSampler, input.uv + offset) * weights[i];
1059
+ }
1060
+
1061
+ return result;
1062
+ }
1063
+ `,
1064
+ });
1065
+ // Bloom composition shader (combines original scene with bloom)
1066
+ const bloomComposeShader = this.device.createShaderModule({
1067
+ label: "bloom compose",
1068
+ code: /* wgsl */ `
1069
+ struct VertexOutput {
1070
+ @builtin(position) position: vec4f,
1071
+ @location(0) uv: vec2f,
1072
+ };
1073
+
1074
+ @vertex fn vs(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput {
1075
+ var output: VertexOutput;
1076
+ let x = f32((vertexIndex << 1u) & 2u) * 2.0 - 1.0;
1077
+ let y = f32(vertexIndex & 2u) * 2.0 - 1.0;
1078
+ output.position = vec4f(x, y, 0.0, 1.0);
1079
+ output.uv = vec2f(x * 0.5 + 0.5, 1.0 - (y * 0.5 + 0.5));
1080
+ return output;
1081
+ }
1082
+
1083
+ struct BloomComposeUniforms {
1084
+ intensity: f32,
1085
+ _padding1: f32,
1086
+ _padding2: f32,
1087
+ _padding3: f32,
1088
+ _padding4: f32,
1089
+ _padding5: f32,
1090
+ _padding6: f32,
1091
+ _padding7: f32,
1092
+ };
1093
+
1094
+ @group(0) @binding(0) var sceneTexture: texture_2d<f32>;
1095
+ @group(0) @binding(1) var sceneSampler: sampler;
1096
+ @group(0) @binding(2) var bloomTexture: texture_2d<f32>;
1097
+ @group(0) @binding(3) var bloomSampler: sampler;
1098
+ @group(0) @binding(4) var<uniform> composeUniforms: BloomComposeUniforms;
1099
+
1100
+ @fragment fn fs(input: VertexOutput) -> @location(0) vec4f {
1101
+ let scene = textureSample(sceneTexture, sceneSampler, input.uv);
1102
+ let bloom = textureSample(bloomTexture, bloomSampler, input.uv);
1103
+ // Additive blending with intensity control
1104
+ let result = scene.rgb + bloom.rgb * composeUniforms.intensity;
1105
+ return vec4f(result, scene.a);
1106
+ }
1107
+ `,
1108
+ });
1109
+ // Create uniform buffer for blur direction (minimum 32 bytes for WebGPU)
1110
+ const blurDirectionBuffer = this.device.createBuffer({
1111
+ label: "blur direction",
1112
+ size: 32, // Minimum 32 bytes required for uniform buffers in WebGPU
1113
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
1114
+ });
1115
+ // Create uniform buffer for bloom intensity (minimum 32 bytes for WebGPU)
1116
+ const bloomIntensityBuffer = this.device.createBuffer({
1117
+ label: "bloom intensity",
1118
+ size: 32, // Minimum 32 bytes required for uniform buffers in WebGPU
1119
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
1120
+ });
1121
+ // Create uniform buffer for bloom threshold (minimum 32 bytes for WebGPU)
1122
+ const bloomThresholdBuffer = this.device.createBuffer({
1123
+ label: "bloom threshold",
1124
+ size: 32, // Minimum 32 bytes required for uniform buffers in WebGPU
1125
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
1126
+ });
1127
+ // Set default bloom values
1128
+ const intensityData = new Float32Array(8); // f32 + 7 padding floats = 8 floats = 32 bytes
1129
+ intensityData[0] = this.bloomIntensity;
1130
+ this.device.queue.writeBuffer(bloomIntensityBuffer, 0, intensityData);
1131
+ const thresholdData = new Float32Array(8); // f32 + 7 padding floats = 8 floats = 32 bytes
1132
+ thresholdData[0] = this.bloomThreshold;
1133
+ this.device.queue.writeBuffer(bloomThresholdBuffer, 0, thresholdData);
1134
+ // Create linear sampler for post-processing
1135
+ const linearSampler = this.device.createSampler({
1136
+ magFilter: "linear",
1137
+ minFilter: "linear",
1138
+ addressModeU: "clamp-to-edge",
1139
+ addressModeV: "clamp-to-edge",
1140
+ });
1141
+ // Bloom extraction pipeline
1142
+ this.bloomExtractPipeline = this.device.createRenderPipeline({
1143
+ label: "bloom extract",
1144
+ layout: "auto",
1145
+ vertex: {
1146
+ module: bloomExtractShader,
1147
+ entryPoint: "vs",
1148
+ },
1149
+ fragment: {
1150
+ module: bloomExtractShader,
1151
+ entryPoint: "fs",
1152
+ targets: [{ format: this.presentationFormat }],
1153
+ },
1154
+ primitive: { topology: "triangle-list" },
1155
+ });
1156
+ // Bloom blur pipeline
1157
+ this.bloomBlurPipeline = this.device.createRenderPipeline({
1158
+ label: "bloom blur",
1159
+ layout: "auto",
1160
+ vertex: {
1161
+ module: bloomBlurShader,
1162
+ entryPoint: "vs",
1163
+ },
1164
+ fragment: {
1165
+ module: bloomBlurShader,
1166
+ entryPoint: "fs",
1167
+ targets: [{ format: this.presentationFormat }],
1168
+ },
1169
+ primitive: { topology: "triangle-list" },
1170
+ });
1171
+ // Bloom composition pipeline
1172
+ this.bloomComposePipeline = this.device.createRenderPipeline({
1173
+ label: "bloom compose",
1174
+ layout: "auto",
1175
+ vertex: {
1176
+ module: bloomComposeShader,
1177
+ entryPoint: "vs",
1178
+ },
1179
+ fragment: {
1180
+ module: bloomComposeShader,
1181
+ entryPoint: "fs",
1182
+ targets: [{ format: this.presentationFormat }],
1183
+ },
1184
+ primitive: { topology: "triangle-list" },
1185
+ });
1186
+ // Store buffers and sampler for later use
1187
+ this.blurDirectionBuffer = blurDirectionBuffer;
1188
+ this.bloomIntensityBuffer = bloomIntensityBuffer;
1189
+ this.bloomThresholdBuffer = bloomThresholdBuffer;
1190
+ this.linearSampler = linearSampler;
1191
+ }
926
1192
  // Step 3: Setup canvas resize handling
927
1193
  setupResize() {
928
1194
  this.resizeObserver = new ResizeObserver(() => this.handleResize());
@@ -952,17 +1218,47 @@ export class Engine {
952
1218
  format: "depth24plus-stencil8",
953
1219
  usage: GPUTextureUsage.RENDER_ATTACHMENT,
954
1220
  });
1221
+ // Create scene render texture (non-multisampled for post-processing)
1222
+ this.sceneRenderTexture = this.device.createTexture({
1223
+ label: "scene render texture",
1224
+ size: [width, height],
1225
+ format: this.presentationFormat,
1226
+ usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING,
1227
+ });
1228
+ this.sceneRenderTextureView = this.sceneRenderTexture.createView();
1229
+ // Create bloom textures (half resolution for performance)
1230
+ const bloomWidth = Math.floor(width / 2);
1231
+ const bloomHeight = Math.floor(height / 2);
1232
+ this.bloomExtractTexture = this.device.createTexture({
1233
+ label: "bloom extract",
1234
+ size: [bloomWidth, bloomHeight],
1235
+ format: this.presentationFormat,
1236
+ usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING,
1237
+ });
1238
+ this.bloomBlurTexture1 = this.device.createTexture({
1239
+ label: "bloom blur 1",
1240
+ size: [bloomWidth, bloomHeight],
1241
+ format: this.presentationFormat,
1242
+ usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING,
1243
+ });
1244
+ this.bloomBlurTexture2 = this.device.createTexture({
1245
+ label: "bloom blur 2",
1246
+ size: [bloomWidth, bloomHeight],
1247
+ format: this.presentationFormat,
1248
+ usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING,
1249
+ });
955
1250
  const depthTextureView = this.depthTexture.createView();
1251
+ // Render scene to texture instead of directly to canvas
956
1252
  const colorAttachment = this.sampleCount > 1
957
1253
  ? {
958
1254
  view: this.multisampleTexture.createView(),
959
- resolveTarget: this.context.getCurrentTexture().createView(),
1255
+ resolveTarget: this.sceneRenderTextureView,
960
1256
  clearValue: { r: 0, g: 0, b: 0, a: 0 },
961
1257
  loadOp: "clear",
962
1258
  storeOp: "store",
963
1259
  }
964
1260
  : {
965
- view: this.context.getCurrentTexture().createView(),
1261
+ view: this.sceneRenderTextureView,
966
1262
  clearValue: { r: 0, g: 0, b: 0, a: 0 },
967
1263
  loadOp: "clear",
968
1264
  storeOp: "store",
@@ -1217,8 +1513,7 @@ export class Engine {
1217
1513
  usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
1218
1514
  });
1219
1515
  this.device.queue.writeBuffer(materialUniformBuffer, 0, materialUniformData);
1220
- // Create bind groups using the shared bind group layout
1221
- // All pipelines (main, eye, hair multiply, hair opaque) use the same shader and layout
1516
+ // Create bind groups using the shared bind group layout - All pipelines (main, eye, hair multiply, hair opaque) use the same shader and layout
1222
1517
  const bindGroup = this.device.createBindGroup({
1223
1518
  label: `material bind group: ${mat.name}`,
1224
1519
  layout: this.hairBindGroupLayout,
@@ -1266,8 +1561,7 @@ export class Engine {
1266
1561
  isTransparent,
1267
1562
  });
1268
1563
  }
1269
- // Outline for all materials (including transparent)
1270
- // Edge flag is at bit 4 (0x10) in PMX format, not bit 0 (0x01)
1564
+ // Outline for all materials (including transparent) - Edge flag is at bit 4 (0x10) in PMX format, not bit 0 (0x01)
1271
1565
  if ((mat.edgeFlag & 0x10) !== 0 && mat.edgeSize > 0) {
1272
1566
  const materialUniformData = new Float32Array(8);
1273
1567
  materialUniformData[0] = mat.edgeColor[0];
@@ -1388,8 +1682,8 @@ export class Engine {
1388
1682
  pass.setVertexBuffer(2, this.weightsBuffer);
1389
1683
  pass.setIndexBuffer(this.indexBuffer, "uint32");
1390
1684
  this.drawCallCount = 0;
1391
- // === PASS 1: Opaque non-eye, non-hair (face, body, etc) ===
1392
- this.drawOutlines(pass, false); // Opaque outlines
1685
+ // PASS 1: Opaque non-eye, non-hair (face, body, etc)
1686
+ // this.drawOutlines(pass, false) // Opaque outlines
1393
1687
  pass.setPipeline(this.pipeline);
1394
1688
  for (const draw of this.opaqueNonEyeNonHairDraws) {
1395
1689
  if (draw.count > 0) {
@@ -1398,7 +1692,7 @@ export class Engine {
1398
1692
  this.drawCallCount++;
1399
1693
  }
1400
1694
  }
1401
- // === PASS 2: Eyes (writes stencil = 1) ===
1695
+ // PASS 2: Eyes (writes stencil = 1)
1402
1696
  pass.setPipeline(this.eyePipeline);
1403
1697
  pass.setStencilReference(1); // Set stencil reference value to 1
1404
1698
  for (const draw of this.eyeDraws) {
@@ -1408,8 +1702,7 @@ export class Engine {
1408
1702
  this.drawCallCount++;
1409
1703
  }
1410
1704
  }
1411
- // === PASS 3a: Hair over eyes (stencil == 1, multiply blend) ===
1412
- // Draw hair geometry first to establish depth
1705
+ // PASS 3a: Hair over eyes (stencil == 1, multiply blend) - Draw hair geometry first to establish depth
1413
1706
  pass.setPipeline(this.hairMultiplyPipeline);
1414
1707
  pass.setStencilReference(1); // Check against stencil value 1
1415
1708
  for (const draw of this.hairDraws) {
@@ -1419,9 +1712,7 @@ export class Engine {
1419
1712
  this.drawCallCount++;
1420
1713
  }
1421
1714
  }
1422
- // === PASS 3a.5: Hair outlines over eyes (stencil == 1, depth test to only draw near hair) ===
1423
- // Use depth compare "less-equal" with the hair depth to only draw outline where hair exists
1424
- // The outline is expanded outward, so we need to ensure it only appears near the hair edge
1715
+ // PASS 3a.5: Hair outlines over eyes (stencil == 1, depth test to only draw near hair)
1425
1716
  pass.setPipeline(this.hairOutlineOverEyesPipeline);
1426
1717
  pass.setStencilReference(1); // Check against stencil value 1 (with equal test)
1427
1718
  for (const draw of this.hairOutlineDraws) {
@@ -1430,7 +1721,7 @@ export class Engine {
1430
1721
  pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
1431
1722
  }
1432
1723
  }
1433
- // === PASS 3b: Hair over non-eyes (stencil != 1, opaque) ===
1724
+ // PASS 3b: Hair over non-eyes (stencil != 1, opaque)
1434
1725
  pass.setPipeline(this.hairOpaquePipeline);
1435
1726
  pass.setStencilReference(1); // Check against stencil value 1 (with not-equal test)
1436
1727
  for (const draw of this.hairDraws) {
@@ -1440,8 +1731,7 @@ export class Engine {
1440
1731
  this.drawCallCount++;
1441
1732
  }
1442
1733
  }
1443
- // === PASS 3b.5: Hair outlines over non-eyes (stencil != 1) ===
1444
- // Draw hair outlines after hair geometry, so they only appear where hair exists
1734
+ // PASS 3b.5: Hair outlines over non-eyes (stencil != 1) - Draw hair outlines after hair geometry, so they only appear where hair exists
1445
1735
  pass.setPipeline(this.hairOutlinePipeline);
1446
1736
  pass.setStencilReference(1); // Check against stencil value 1 (with not-equal test)
1447
1737
  for (const draw of this.hairOutlineDraws) {
@@ -1450,7 +1740,8 @@ export class Engine {
1450
1740
  pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
1451
1741
  }
1452
1742
  }
1453
- // === PASS 4: Transparent non-eye, non-hair ===
1743
+ this.drawOutlines(pass, false); // Opaque outlines
1744
+ // PASS 4: Transparent non-eye, non-hair
1454
1745
  pass.setPipeline(this.pipeline);
1455
1746
  for (const draw of this.transparentNonEyeNonHairDraws) {
1456
1747
  if (draw.count > 0) {
@@ -1462,9 +1753,136 @@ export class Engine {
1462
1753
  this.drawOutlines(pass, true); // Transparent outlines
1463
1754
  pass.end();
1464
1755
  this.device.queue.submit([encoder.finish()]);
1756
+ // Apply bloom post-processing
1757
+ this.applyBloom();
1465
1758
  this.updateStats(performance.now() - currentTime);
1466
1759
  }
1467
1760
  }
1761
+ // Apply bloom post-processing
1762
+ applyBloom() {
1763
+ if (!this.sceneRenderTexture || !this.bloomExtractTexture) {
1764
+ return;
1765
+ }
1766
+ // Update bloom parameters
1767
+ const thresholdData = new Float32Array(8);
1768
+ thresholdData[0] = this.bloomThreshold;
1769
+ this.device.queue.writeBuffer(this.bloomThresholdBuffer, 0, thresholdData);
1770
+ const intensityData = new Float32Array(8);
1771
+ intensityData[0] = this.bloomIntensity;
1772
+ this.device.queue.writeBuffer(this.bloomIntensityBuffer, 0, intensityData);
1773
+ const encoder = this.device.createCommandEncoder();
1774
+ const width = this.canvas.width;
1775
+ const height = this.canvas.height;
1776
+ const bloomWidth = Math.floor(width / 2);
1777
+ const bloomHeight = Math.floor(height / 2);
1778
+ // Pass 1: Extract bright areas (downsample to half resolution)
1779
+ const extractPass = encoder.beginRenderPass({
1780
+ label: "bloom extract",
1781
+ colorAttachments: [
1782
+ {
1783
+ view: this.bloomExtractTexture.createView(),
1784
+ clearValue: { r: 0, g: 0, b: 0, a: 0 },
1785
+ loadOp: "clear",
1786
+ storeOp: "store",
1787
+ },
1788
+ ],
1789
+ });
1790
+ const extractBindGroup = this.device.createBindGroup({
1791
+ layout: this.bloomExtractPipeline.getBindGroupLayout(0),
1792
+ entries: [
1793
+ { binding: 0, resource: this.sceneRenderTexture.createView() },
1794
+ { binding: 1, resource: this.linearSampler },
1795
+ { binding: 2, resource: { buffer: this.bloomThresholdBuffer } },
1796
+ ],
1797
+ });
1798
+ extractPass.setPipeline(this.bloomExtractPipeline);
1799
+ extractPass.setBindGroup(0, extractBindGroup);
1800
+ extractPass.draw(6, 1, 0, 0);
1801
+ extractPass.end();
1802
+ // Pass 2: Horizontal blur
1803
+ const hBlurData = new Float32Array(4); // vec2f + padding = 4 floats
1804
+ hBlurData[0] = 1.0;
1805
+ hBlurData[1] = 0.0;
1806
+ this.device.queue.writeBuffer(this.blurDirectionBuffer, 0, hBlurData);
1807
+ const blurHPass = encoder.beginRenderPass({
1808
+ label: "bloom blur horizontal",
1809
+ colorAttachments: [
1810
+ {
1811
+ view: this.bloomBlurTexture1.createView(),
1812
+ clearValue: { r: 0, g: 0, b: 0, a: 0 },
1813
+ loadOp: "clear",
1814
+ storeOp: "store",
1815
+ },
1816
+ ],
1817
+ });
1818
+ const blurHBindGroup = this.device.createBindGroup({
1819
+ layout: this.bloomBlurPipeline.getBindGroupLayout(0),
1820
+ entries: [
1821
+ { binding: 0, resource: this.bloomExtractTexture.createView() },
1822
+ { binding: 1, resource: this.linearSampler },
1823
+ { binding: 2, resource: { buffer: this.blurDirectionBuffer } },
1824
+ ],
1825
+ });
1826
+ blurHPass.setPipeline(this.bloomBlurPipeline);
1827
+ blurHPass.setBindGroup(0, blurHBindGroup);
1828
+ blurHPass.draw(6, 1, 0, 0);
1829
+ blurHPass.end();
1830
+ // Pass 3: Vertical blur
1831
+ const vBlurData = new Float32Array(4); // vec2f + padding = 4 floats
1832
+ vBlurData[0] = 0.0;
1833
+ vBlurData[1] = 1.0;
1834
+ this.device.queue.writeBuffer(this.blurDirectionBuffer, 0, vBlurData);
1835
+ const blurVPass = encoder.beginRenderPass({
1836
+ label: "bloom blur vertical",
1837
+ colorAttachments: [
1838
+ {
1839
+ view: this.bloomBlurTexture2.createView(),
1840
+ clearValue: { r: 0, g: 0, b: 0, a: 0 },
1841
+ loadOp: "clear",
1842
+ storeOp: "store",
1843
+ },
1844
+ ],
1845
+ });
1846
+ const blurVBindGroup = this.device.createBindGroup({
1847
+ layout: this.bloomBlurPipeline.getBindGroupLayout(0),
1848
+ entries: [
1849
+ { binding: 0, resource: this.bloomBlurTexture1.createView() },
1850
+ { binding: 1, resource: this.linearSampler },
1851
+ { binding: 2, resource: { buffer: this.blurDirectionBuffer } },
1852
+ ],
1853
+ });
1854
+ blurVPass.setPipeline(this.bloomBlurPipeline);
1855
+ blurVPass.setBindGroup(0, blurVBindGroup);
1856
+ blurVPass.draw(6, 1, 0, 0);
1857
+ blurVPass.end();
1858
+ // Pass 4: Compose scene + bloom to canvas
1859
+ const composePass = encoder.beginRenderPass({
1860
+ label: "bloom compose",
1861
+ colorAttachments: [
1862
+ {
1863
+ view: this.context.getCurrentTexture().createView(),
1864
+ clearValue: { r: 0, g: 0, b: 0, a: 0 },
1865
+ loadOp: "clear",
1866
+ storeOp: "store",
1867
+ },
1868
+ ],
1869
+ });
1870
+ const composeBindGroup = this.device.createBindGroup({
1871
+ layout: this.bloomComposePipeline.getBindGroupLayout(0),
1872
+ entries: [
1873
+ { binding: 0, resource: this.sceneRenderTexture.createView() },
1874
+ { binding: 1, resource: this.linearSampler },
1875
+ { binding: 2, resource: this.bloomBlurTexture2.createView() },
1876
+ { binding: 3, resource: this.linearSampler },
1877
+ { binding: 4, resource: { buffer: this.bloomIntensityBuffer } },
1878
+ ],
1879
+ });
1880
+ composePass.setPipeline(this.bloomComposePipeline);
1881
+ composePass.setBindGroup(0, composeBindGroup);
1882
+ composePass.draw(6, 1, 0, 0);
1883
+ composePass.end();
1884
+ this.device.queue.submit([encoder.finish()]);
1885
+ }
1468
1886
  // Update camera uniform buffer each frame
1469
1887
  updateCameraUniforms() {
1470
1888
  const viewMatrix = this.camera.getViewMatrix();
@@ -1481,10 +1899,12 @@ export class Engine {
1481
1899
  updateRenderTarget() {
1482
1900
  const colorAttachment = this.renderPassDescriptor.colorAttachments[0];
1483
1901
  if (this.sampleCount > 1) {
1484
- colorAttachment.resolveTarget = this.context.getCurrentTexture().createView();
1902
+ // Resolve to scene render texture for post-processing
1903
+ colorAttachment.resolveTarget = this.sceneRenderTextureView;
1485
1904
  }
1486
1905
  else {
1487
- colorAttachment.view = this.context.getCurrentTexture().createView();
1906
+ // Render directly to scene render texture
1907
+ colorAttachment.view = this.sceneRenderTextureView;
1488
1908
  }
1489
1909
  }
1490
1910
  // Update model pose and physics
@@ -1 +1 @@
1
- {"version":3,"file":"pmx-loader.d.ts","sourceRoot":"","sources":["../src/pmx-loader.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAA+C,MAAM,SAAS,CAAA;AAI5E,qBAAa,SAAS;IACpB,OAAO,CAAC,IAAI,CAAU;IACtB,OAAO,CAAC,MAAM,CAAI;IAClB,OAAO,CAAC,OAAO,CAAc;IAC7B,OAAO,CAAC,QAAQ,CAAI;IACpB,OAAO,CAAC,mBAAmB,CAAI;IAC/B,OAAO,CAAC,eAAe,CAAI;IAC3B,OAAO,CAAC,gBAAgB,CAAI;IAC5B,OAAO,CAAC,iBAAiB,CAAI;IAC7B,OAAO,CAAC,aAAa,CAAI;IACzB,OAAO,CAAC,cAAc,CAAI;IAC1B,OAAO,CAAC,kBAAkB,CAAI;IAC9B,OAAO,CAAC,QAAQ,CAAgB;IAChC,OAAO,CAAC,SAAS,CAAiB;IAClC,OAAO,CAAC,KAAK,CAAa;IAC1B,OAAO,CAAC,mBAAmB,CAA4B;IACvD,OAAO,CAAC,OAAO,CAA2B;IAC1C,OAAO,CAAC,QAAQ,CAA0B;IAC1C,OAAO,CAAC,WAAW,CAAkB;IACrC,OAAO,CAAC,MAAM,CAAc;IAE5B,OAAO;WAIM,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,KAAK,CAAC;IAK9C,OAAO,CAAC,KAAK;IAgBb,OAAO,CAAC,WAAW;IA+CnB,OAAO,CAAC,aAAa;IA+FrB,OAAO,CAAC,YAAY;IAWpB,OAAO,CAAC,aAAa;IAkBrB,OAAO,CAAC,cAAc;IA+FtB,OAAO,CAAC,UAAU;IA2IlB,OAAO,CAAC,UAAU;IAyGlB,OAAO,CAAC,iBAAiB;IAgDzB,OAAO,CAAC,gBAAgB;IAyFxB,OAAO,CAAC,WAAW;IAmGnB,OAAO,CAAC,kBAAkB;IAmC1B,OAAO,CAAC,OAAO;IA2If,OAAO,CAAC,QAAQ;IAOhB,OAAO,CAAC,SAAS;IAUjB,OAAO,CAAC,cAAc;IAWtB,OAAO,CAAC,iBAAiB;IAczB,OAAO,CAAC,QAAQ;IAShB,OAAO,CAAC,UAAU;IASlB,OAAO,CAAC,SAAS;IAMjB,OAAO,CAAC,OAAO;IAmBf,OAAO,CAAC,QAAQ;CAIjB"}
1
+ {"version":3,"file":"pmx-loader.d.ts","sourceRoot":"","sources":["../src/pmx-loader.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAA+C,MAAM,SAAS,CAAA;AAI5E,qBAAa,SAAS;IACpB,OAAO,CAAC,IAAI,CAAU;IACtB,OAAO,CAAC,MAAM,CAAI;IAClB,OAAO,CAAC,OAAO,CAAc;IAC7B,OAAO,CAAC,QAAQ,CAAI;IACpB,OAAO,CAAC,mBAAmB,CAAI;IAC/B,OAAO,CAAC,eAAe,CAAI;IAC3B,OAAO,CAAC,gBAAgB,CAAI;IAC5B,OAAO,CAAC,iBAAiB,CAAI;IAC7B,OAAO,CAAC,aAAa,CAAI;IACzB,OAAO,CAAC,cAAc,CAAI;IAC1B,OAAO,CAAC,kBAAkB,CAAI;IAC9B,OAAO,CAAC,QAAQ,CAAgB;IAChC,OAAO,CAAC,SAAS,CAAiB;IAClC,OAAO,CAAC,KAAK,CAAa;IAC1B,OAAO,CAAC,mBAAmB,CAA4B;IACvD,OAAO,CAAC,OAAO,CAA2B;IAC1C,OAAO,CAAC,QAAQ,CAA0B;IAC1C,OAAO,CAAC,WAAW,CAAkB;IACrC,OAAO,CAAC,MAAM,CAAc;IAE5B,OAAO;WAIM,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,KAAK,CAAC;IAK9C,OAAO,CAAC,KAAK;IAgBb,OAAO,CAAC,WAAW;IA+CnB,OAAO,CAAC,aAAa;IA+FrB,OAAO,CAAC,YAAY;IAWpB,OAAO,CAAC,aAAa;IAkBrB,OAAO,CAAC,cAAc;IAyFtB,OAAO,CAAC,UAAU;IA2IlB,OAAO,CAAC,UAAU;IAyGlB,OAAO,CAAC,iBAAiB;IAgDzB,OAAO,CAAC,gBAAgB;IAyFxB,OAAO,CAAC,WAAW;IAmGnB,OAAO,CAAC,kBAAkB;IAmC1B,OAAO,CAAC,OAAO;IA2If,OAAO,CAAC,QAAQ;IAOhB,OAAO,CAAC,SAAS;IAUjB,OAAO,CAAC,cAAc;IAWtB,OAAO,CAAC,iBAAiB;IAczB,OAAO,CAAC,QAAQ;IAShB,OAAO,CAAC,UAAU;IASlB,OAAO,CAAC,SAAS;IAMjB,OAAO,CAAC,OAAO;IAmBf,OAAO,CAAC,QAAQ;CAIjB"}
@@ -251,20 +251,14 @@ export class PmxLoader {
251
251
  materialName.includes("eye") ||
252
252
  materialName.includes("pupil") ||
253
253
  materialName.includes("iris") ||
254
- materialName.includes("目白");
254
+ materialName.includes("目白") ||
255
+ materialName.includes("眼") ||
256
+ materialName.includes("睛") ||
257
+ materialName.includes("眉");
255
258
  // Classify face materials
256
- mat.isFace =
257
- materialName.includes("顔") || // Japanese "face"
258
- materialName.includes("肌") || // Japanese "skin"
259
- materialName.includes("face") ||
260
- materialName.includes("skin") ||
261
- materialName.includes("head");
259
+ mat.isFace = materialName.includes("face") || materialName.includes("脸");
262
260
  // Classify hair materials
263
- mat.isHair =
264
- materialName.includes("髪") || // Japanese "hair"
265
- materialName.includes("前髪") || // Japanese "bangs"
266
- materialName.includes("hair_f") ||
267
- materialName.includes("头发");
261
+ mat.isHair = materialName.includes("hair_f");
268
262
  this.materials.push(mat);
269
263
  }
270
264
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "reze-engine",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
4
4
  "description": "A WebGPU-based MMD model renderer",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",