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/README.md +99 -99
- package/dist/engine.d.ts +18 -0
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +449 -29
- package/dist/pmx-loader.d.ts.map +1 -1
- package/dist/pmx-loader.js +6 -12
- package/package.json +1 -1
- package/src/camera.ts +358 -358
- package/src/engine.ts +503 -29
- package/src/math.ts +546 -546
- package/src/model.ts +421 -421
- package/src/physics.ts +680 -680
- package/src/pmx-loader.ts +1054 -1060
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
|
-
|
|
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.
|
|
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.
|
|
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
|
-
//
|
|
1392
|
-
this.drawOutlines(pass, false)
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
1902
|
+
// Resolve to scene render texture for post-processing
|
|
1903
|
+
colorAttachment.resolveTarget = this.sceneRenderTextureView;
|
|
1485
1904
|
}
|
|
1486
1905
|
else {
|
|
1487
|
-
|
|
1906
|
+
// Render directly to scene render texture
|
|
1907
|
+
colorAttachment.view = this.sceneRenderTextureView;
|
|
1488
1908
|
}
|
|
1489
1909
|
}
|
|
1490
1910
|
// Update model pose and physics
|
package/dist/pmx-loader.d.ts.map
CHANGED
|
@@ -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;
|
|
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"}
|
package/dist/pmx-loader.js
CHANGED
|
@@ -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
|
}
|