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/src/engine.ts
CHANGED
|
@@ -45,6 +45,25 @@ export class Engine {
|
|
|
45
45
|
private multisampleTexture!: GPUTexture
|
|
46
46
|
private readonly sampleCount = 4 // MSAA 4x
|
|
47
47
|
private renderPassDescriptor!: GPURenderPassDescriptor
|
|
48
|
+
// Bloom post-processing textures
|
|
49
|
+
private sceneRenderTexture!: GPUTexture
|
|
50
|
+
private sceneRenderTextureView!: GPUTextureView
|
|
51
|
+
private bloomExtractTexture!: GPUTexture
|
|
52
|
+
private bloomBlurTexture1!: GPUTexture
|
|
53
|
+
private bloomBlurTexture2!: GPUTexture
|
|
54
|
+
// Bloom post-processing pipelines
|
|
55
|
+
private bloomExtractPipeline!: GPURenderPipeline
|
|
56
|
+
private bloomBlurPipeline!: GPURenderPipeline
|
|
57
|
+
private bloomComposePipeline!: GPURenderPipeline
|
|
58
|
+
// Fullscreen quad for post-processing
|
|
59
|
+
private fullscreenQuadBuffer!: GPUBuffer
|
|
60
|
+
private blurDirectionBuffer!: GPUBuffer
|
|
61
|
+
private bloomIntensityBuffer!: GPUBuffer
|
|
62
|
+
private bloomThresholdBuffer!: GPUBuffer
|
|
63
|
+
private linearSampler!: GPUSampler
|
|
64
|
+
// Bloom settings
|
|
65
|
+
public bloomThreshold: number = 0.3
|
|
66
|
+
public bloomIntensity: number = 0.14
|
|
48
67
|
private currentModel: Model | null = null
|
|
49
68
|
private modelDir: string = ""
|
|
50
69
|
private physics: Physics | null = null
|
|
@@ -96,6 +115,8 @@ export class Engine {
|
|
|
96
115
|
this.setupCamera()
|
|
97
116
|
this.setupLighting()
|
|
98
117
|
this.createPipelines()
|
|
118
|
+
this.createFullscreenQuad()
|
|
119
|
+
this.createBloomPipelines()
|
|
99
120
|
this.setupResize()
|
|
100
121
|
}
|
|
101
122
|
|
|
@@ -325,9 +346,8 @@ export class Engine {
|
|
|
325
346
|
discard;
|
|
326
347
|
}
|
|
327
348
|
|
|
328
|
-
// For hair-over-eyes effect: simple half-transparent overlay
|
|
329
|
-
|
|
330
|
-
let overlayAlpha = finalAlpha * 0.6;
|
|
349
|
+
// For hair-over-eyes effect: simple half-transparent overlay - Use 50% opacity to create a semi-transparent hair color overlay
|
|
350
|
+
let overlayAlpha = finalAlpha * 0.5;
|
|
331
351
|
|
|
332
352
|
return vec4f(clamp(color, vec3f(0.0), vec3f(1.0)), overlayAlpha);
|
|
333
353
|
}
|
|
@@ -566,8 +586,7 @@ export class Engine {
|
|
|
566
586
|
},
|
|
567
587
|
})
|
|
568
588
|
|
|
569
|
-
// Hair outline pipeline: draws hair outlines over non-eyes (stencil != 1)
|
|
570
|
-
// Drawn after hair geometry, so depth testing ensures outlines only appear where hair exists
|
|
589
|
+
// 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
|
|
571
590
|
this.hairOutlinePipeline = this.device.createRenderPipeline({
|
|
572
591
|
label: "hair outline pipeline",
|
|
573
592
|
layout: outlinePipelineLayout,
|
|
@@ -649,8 +668,7 @@ export class Engine {
|
|
|
649
668
|
},
|
|
650
669
|
})
|
|
651
670
|
|
|
652
|
-
// Hair outline pipeline for over eyes: draws where stencil == 1, but only where hair depth exists
|
|
653
|
-
// Uses depth compare "equal" with a small bias to only appear where hair geometry exists
|
|
671
|
+
// 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
|
|
654
672
|
this.hairOutlineOverEyesPipeline = this.device.createRenderPipeline({
|
|
655
673
|
label: "hair outline over eyes pipeline",
|
|
656
674
|
layout: outlinePipelineLayout,
|
|
@@ -768,8 +786,7 @@ export class Engine {
|
|
|
768
786
|
format: this.presentationFormat,
|
|
769
787
|
blend: {
|
|
770
788
|
color: {
|
|
771
|
-
// Simple half-transparent overlay effect
|
|
772
|
-
// Blend: hairColor * overlayAlpha + eyeColor * (1 - overlayAlpha)
|
|
789
|
+
// Simple half-transparent overlay effect - Blend: hairColor * overlayAlpha + eyeColor * (1 - overlayAlpha)
|
|
773
790
|
srcFactor: "src-alpha",
|
|
774
791
|
dstFactor: "one-minus-src-alpha",
|
|
775
792
|
operation: "add",
|
|
@@ -978,6 +995,286 @@ export class Engine {
|
|
|
978
995
|
})
|
|
979
996
|
}
|
|
980
997
|
|
|
998
|
+
// Create fullscreen quad for post-processing
|
|
999
|
+
private createFullscreenQuad() {
|
|
1000
|
+
// Fullscreen quad vertices: two triangles covering the entire screen - Format: position (x, y), uv (u, v)
|
|
1001
|
+
const quadVertices = new Float32Array([
|
|
1002
|
+
// Triangle 1
|
|
1003
|
+
-1.0,
|
|
1004
|
+
-1.0,
|
|
1005
|
+
0.0,
|
|
1006
|
+
0.0, // bottom-left
|
|
1007
|
+
1.0,
|
|
1008
|
+
-1.0,
|
|
1009
|
+
1.0,
|
|
1010
|
+
0.0, // bottom-right
|
|
1011
|
+
-1.0,
|
|
1012
|
+
1.0,
|
|
1013
|
+
0.0,
|
|
1014
|
+
1.0, // top-left
|
|
1015
|
+
// Triangle 2
|
|
1016
|
+
-1.0,
|
|
1017
|
+
1.0,
|
|
1018
|
+
0.0,
|
|
1019
|
+
1.0, // top-left
|
|
1020
|
+
1.0,
|
|
1021
|
+
-1.0,
|
|
1022
|
+
1.0,
|
|
1023
|
+
0.0, // bottom-right
|
|
1024
|
+
1.0,
|
|
1025
|
+
1.0,
|
|
1026
|
+
1.0,
|
|
1027
|
+
1.0, // top-right
|
|
1028
|
+
])
|
|
1029
|
+
|
|
1030
|
+
this.fullscreenQuadBuffer = this.device.createBuffer({
|
|
1031
|
+
label: "fullscreen quad",
|
|
1032
|
+
size: quadVertices.byteLength,
|
|
1033
|
+
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
|
|
1034
|
+
})
|
|
1035
|
+
this.device.queue.writeBuffer(this.fullscreenQuadBuffer, 0, quadVertices)
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
// Create bloom post-processing pipelines
|
|
1039
|
+
private createBloomPipelines() {
|
|
1040
|
+
// Bloom extraction shader (extracts bright areas)
|
|
1041
|
+
const bloomExtractShader = this.device.createShaderModule({
|
|
1042
|
+
label: "bloom extract",
|
|
1043
|
+
code: /* wgsl */ `
|
|
1044
|
+
struct VertexOutput {
|
|
1045
|
+
@builtin(position) position: vec4f,
|
|
1046
|
+
@location(0) uv: vec2f,
|
|
1047
|
+
};
|
|
1048
|
+
|
|
1049
|
+
@vertex fn vs(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput {
|
|
1050
|
+
var output: VertexOutput;
|
|
1051
|
+
// Generate fullscreen quad from vertex index
|
|
1052
|
+
let x = f32((vertexIndex << 1u) & 2u) * 2.0 - 1.0;
|
|
1053
|
+
let y = f32(vertexIndex & 2u) * 2.0 - 1.0;
|
|
1054
|
+
output.position = vec4f(x, y, 0.0, 1.0);
|
|
1055
|
+
output.uv = vec2f(x * 0.5 + 0.5, 1.0 - (y * 0.5 + 0.5));
|
|
1056
|
+
return output;
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
struct BloomExtractUniforms {
|
|
1060
|
+
threshold: f32,
|
|
1061
|
+
_padding1: f32,
|
|
1062
|
+
_padding2: f32,
|
|
1063
|
+
_padding3: f32,
|
|
1064
|
+
_padding4: f32,
|
|
1065
|
+
_padding5: f32,
|
|
1066
|
+
_padding6: f32,
|
|
1067
|
+
_padding7: f32,
|
|
1068
|
+
};
|
|
1069
|
+
|
|
1070
|
+
@group(0) @binding(0) var inputTexture: texture_2d<f32>;
|
|
1071
|
+
@group(0) @binding(1) var inputSampler: sampler;
|
|
1072
|
+
@group(0) @binding(2) var<uniform> extractUniforms: BloomExtractUniforms;
|
|
1073
|
+
|
|
1074
|
+
@fragment fn fs(input: VertexOutput) -> @location(0) vec4f {
|
|
1075
|
+
let color = textureSample(inputTexture, inputSampler, input.uv);
|
|
1076
|
+
// Extract bright areas above threshold
|
|
1077
|
+
let threshold = extractUniforms.threshold;
|
|
1078
|
+
let bloom = max(vec3f(0.0), color.rgb - vec3f(threshold)) / max(0.001, 1.0 - threshold);
|
|
1079
|
+
return vec4f(bloom, color.a);
|
|
1080
|
+
}
|
|
1081
|
+
`,
|
|
1082
|
+
})
|
|
1083
|
+
|
|
1084
|
+
// Bloom blur shader (gaussian blur - can be used for both horizontal and vertical)
|
|
1085
|
+
const bloomBlurShader = this.device.createShaderModule({
|
|
1086
|
+
label: "bloom blur",
|
|
1087
|
+
code: /* wgsl */ `
|
|
1088
|
+
struct VertexOutput {
|
|
1089
|
+
@builtin(position) position: vec4f,
|
|
1090
|
+
@location(0) uv: vec2f,
|
|
1091
|
+
};
|
|
1092
|
+
|
|
1093
|
+
@vertex fn vs(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput {
|
|
1094
|
+
var output: VertexOutput;
|
|
1095
|
+
let x = f32((vertexIndex << 1u) & 2u) * 2.0 - 1.0;
|
|
1096
|
+
let y = f32(vertexIndex & 2u) * 2.0 - 1.0;
|
|
1097
|
+
output.position = vec4f(x, y, 0.0, 1.0);
|
|
1098
|
+
output.uv = vec2f(x * 0.5 + 0.5, 1.0 - (y * 0.5 + 0.5));
|
|
1099
|
+
return output;
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
struct BlurUniforms {
|
|
1103
|
+
direction: vec2f,
|
|
1104
|
+
_padding1: f32,
|
|
1105
|
+
_padding2: f32,
|
|
1106
|
+
_padding3: f32,
|
|
1107
|
+
_padding4: f32,
|
|
1108
|
+
_padding5: f32,
|
|
1109
|
+
_padding6: f32,
|
|
1110
|
+
};
|
|
1111
|
+
|
|
1112
|
+
@group(0) @binding(0) var inputTexture: texture_2d<f32>;
|
|
1113
|
+
@group(0) @binding(1) var inputSampler: sampler;
|
|
1114
|
+
@group(0) @binding(2) var<uniform> blurUniforms: BlurUniforms;
|
|
1115
|
+
|
|
1116
|
+
// 9-tap gaussian blur
|
|
1117
|
+
@fragment fn fs(input: VertexOutput) -> @location(0) vec4f {
|
|
1118
|
+
let texelSize = 1.0 / vec2f(textureDimensions(inputTexture));
|
|
1119
|
+
var result = vec4f(0.0);
|
|
1120
|
+
|
|
1121
|
+
// Gaussian weights for 9-tap filter
|
|
1122
|
+
let weights = array<f32, 9>(
|
|
1123
|
+
0.01621622, 0.05405405, 0.12162162,
|
|
1124
|
+
0.19459459, 0.22702703,
|
|
1125
|
+
0.19459459, 0.12162162, 0.05405405, 0.01621622
|
|
1126
|
+
);
|
|
1127
|
+
|
|
1128
|
+
let offsets = array<f32, 9>(-4.0, -3.0, -2.0, -1.0, 0.0, 1.0, 2.0, 3.0, 4.0);
|
|
1129
|
+
|
|
1130
|
+
for (var i = 0u; i < 9u; i++) {
|
|
1131
|
+
let offset = offsets[i] * texelSize * blurUniforms.direction;
|
|
1132
|
+
result += textureSample(inputTexture, inputSampler, input.uv + offset) * weights[i];
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
return result;
|
|
1136
|
+
}
|
|
1137
|
+
`,
|
|
1138
|
+
})
|
|
1139
|
+
|
|
1140
|
+
// Bloom composition shader (combines original scene with bloom)
|
|
1141
|
+
const bloomComposeShader = this.device.createShaderModule({
|
|
1142
|
+
label: "bloom compose",
|
|
1143
|
+
code: /* wgsl */ `
|
|
1144
|
+
struct VertexOutput {
|
|
1145
|
+
@builtin(position) position: vec4f,
|
|
1146
|
+
@location(0) uv: vec2f,
|
|
1147
|
+
};
|
|
1148
|
+
|
|
1149
|
+
@vertex fn vs(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput {
|
|
1150
|
+
var output: VertexOutput;
|
|
1151
|
+
let x = f32((vertexIndex << 1u) & 2u) * 2.0 - 1.0;
|
|
1152
|
+
let y = f32(vertexIndex & 2u) * 2.0 - 1.0;
|
|
1153
|
+
output.position = vec4f(x, y, 0.0, 1.0);
|
|
1154
|
+
output.uv = vec2f(x * 0.5 + 0.5, 1.0 - (y * 0.5 + 0.5));
|
|
1155
|
+
return output;
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
struct BloomComposeUniforms {
|
|
1159
|
+
intensity: f32,
|
|
1160
|
+
_padding1: f32,
|
|
1161
|
+
_padding2: f32,
|
|
1162
|
+
_padding3: f32,
|
|
1163
|
+
_padding4: f32,
|
|
1164
|
+
_padding5: f32,
|
|
1165
|
+
_padding6: f32,
|
|
1166
|
+
_padding7: f32,
|
|
1167
|
+
};
|
|
1168
|
+
|
|
1169
|
+
@group(0) @binding(0) var sceneTexture: texture_2d<f32>;
|
|
1170
|
+
@group(0) @binding(1) var sceneSampler: sampler;
|
|
1171
|
+
@group(0) @binding(2) var bloomTexture: texture_2d<f32>;
|
|
1172
|
+
@group(0) @binding(3) var bloomSampler: sampler;
|
|
1173
|
+
@group(0) @binding(4) var<uniform> composeUniforms: BloomComposeUniforms;
|
|
1174
|
+
|
|
1175
|
+
@fragment fn fs(input: VertexOutput) -> @location(0) vec4f {
|
|
1176
|
+
let scene = textureSample(sceneTexture, sceneSampler, input.uv);
|
|
1177
|
+
let bloom = textureSample(bloomTexture, bloomSampler, input.uv);
|
|
1178
|
+
// Additive blending with intensity control
|
|
1179
|
+
let result = scene.rgb + bloom.rgb * composeUniforms.intensity;
|
|
1180
|
+
return vec4f(result, scene.a);
|
|
1181
|
+
}
|
|
1182
|
+
`,
|
|
1183
|
+
})
|
|
1184
|
+
|
|
1185
|
+
// Create uniform buffer for blur direction (minimum 32 bytes for WebGPU)
|
|
1186
|
+
const blurDirectionBuffer = this.device.createBuffer({
|
|
1187
|
+
label: "blur direction",
|
|
1188
|
+
size: 32, // Minimum 32 bytes required for uniform buffers in WebGPU
|
|
1189
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
1190
|
+
})
|
|
1191
|
+
|
|
1192
|
+
// Create uniform buffer for bloom intensity (minimum 32 bytes for WebGPU)
|
|
1193
|
+
const bloomIntensityBuffer = this.device.createBuffer({
|
|
1194
|
+
label: "bloom intensity",
|
|
1195
|
+
size: 32, // Minimum 32 bytes required for uniform buffers in WebGPU
|
|
1196
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
1197
|
+
})
|
|
1198
|
+
|
|
1199
|
+
// Create uniform buffer for bloom threshold (minimum 32 bytes for WebGPU)
|
|
1200
|
+
const bloomThresholdBuffer = this.device.createBuffer({
|
|
1201
|
+
label: "bloom threshold",
|
|
1202
|
+
size: 32, // Minimum 32 bytes required for uniform buffers in WebGPU
|
|
1203
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
1204
|
+
})
|
|
1205
|
+
|
|
1206
|
+
// Set default bloom values
|
|
1207
|
+
const intensityData = new Float32Array(8) // f32 + 7 padding floats = 8 floats = 32 bytes
|
|
1208
|
+
intensityData[0] = this.bloomIntensity
|
|
1209
|
+
this.device.queue.writeBuffer(bloomIntensityBuffer, 0, intensityData)
|
|
1210
|
+
|
|
1211
|
+
const thresholdData = new Float32Array(8) // f32 + 7 padding floats = 8 floats = 32 bytes
|
|
1212
|
+
thresholdData[0] = this.bloomThreshold
|
|
1213
|
+
this.device.queue.writeBuffer(bloomThresholdBuffer, 0, thresholdData)
|
|
1214
|
+
|
|
1215
|
+
// Create linear sampler for post-processing
|
|
1216
|
+
const linearSampler = this.device.createSampler({
|
|
1217
|
+
magFilter: "linear",
|
|
1218
|
+
minFilter: "linear",
|
|
1219
|
+
addressModeU: "clamp-to-edge",
|
|
1220
|
+
addressModeV: "clamp-to-edge",
|
|
1221
|
+
})
|
|
1222
|
+
|
|
1223
|
+
// Bloom extraction pipeline
|
|
1224
|
+
this.bloomExtractPipeline = this.device.createRenderPipeline({
|
|
1225
|
+
label: "bloom extract",
|
|
1226
|
+
layout: "auto",
|
|
1227
|
+
vertex: {
|
|
1228
|
+
module: bloomExtractShader,
|
|
1229
|
+
entryPoint: "vs",
|
|
1230
|
+
},
|
|
1231
|
+
fragment: {
|
|
1232
|
+
module: bloomExtractShader,
|
|
1233
|
+
entryPoint: "fs",
|
|
1234
|
+
targets: [{ format: this.presentationFormat }],
|
|
1235
|
+
},
|
|
1236
|
+
primitive: { topology: "triangle-list" },
|
|
1237
|
+
})
|
|
1238
|
+
|
|
1239
|
+
// Bloom blur pipeline
|
|
1240
|
+
this.bloomBlurPipeline = this.device.createRenderPipeline({
|
|
1241
|
+
label: "bloom blur",
|
|
1242
|
+
layout: "auto",
|
|
1243
|
+
vertex: {
|
|
1244
|
+
module: bloomBlurShader,
|
|
1245
|
+
entryPoint: "vs",
|
|
1246
|
+
},
|
|
1247
|
+
fragment: {
|
|
1248
|
+
module: bloomBlurShader,
|
|
1249
|
+
entryPoint: "fs",
|
|
1250
|
+
targets: [{ format: this.presentationFormat }],
|
|
1251
|
+
},
|
|
1252
|
+
primitive: { topology: "triangle-list" },
|
|
1253
|
+
})
|
|
1254
|
+
|
|
1255
|
+
// Bloom composition pipeline
|
|
1256
|
+
this.bloomComposePipeline = this.device.createRenderPipeline({
|
|
1257
|
+
label: "bloom compose",
|
|
1258
|
+
layout: "auto",
|
|
1259
|
+
vertex: {
|
|
1260
|
+
module: bloomComposeShader,
|
|
1261
|
+
entryPoint: "vs",
|
|
1262
|
+
},
|
|
1263
|
+
fragment: {
|
|
1264
|
+
module: bloomComposeShader,
|
|
1265
|
+
entryPoint: "fs",
|
|
1266
|
+
targets: [{ format: this.presentationFormat }],
|
|
1267
|
+
},
|
|
1268
|
+
primitive: { topology: "triangle-list" },
|
|
1269
|
+
})
|
|
1270
|
+
|
|
1271
|
+
// Store buffers and sampler for later use
|
|
1272
|
+
this.blurDirectionBuffer = blurDirectionBuffer
|
|
1273
|
+
this.bloomIntensityBuffer = bloomIntensityBuffer
|
|
1274
|
+
this.bloomThresholdBuffer = bloomThresholdBuffer
|
|
1275
|
+
this.linearSampler = linearSampler
|
|
1276
|
+
}
|
|
1277
|
+
|
|
981
1278
|
// Step 3: Setup canvas resize handling
|
|
982
1279
|
private setupResize() {
|
|
983
1280
|
this.resizeObserver = new ResizeObserver(() => this.handleResize())
|
|
@@ -1013,19 +1310,51 @@ export class Engine {
|
|
|
1013
1310
|
usage: GPUTextureUsage.RENDER_ATTACHMENT,
|
|
1014
1311
|
})
|
|
1015
1312
|
|
|
1313
|
+
// Create scene render texture (non-multisampled for post-processing)
|
|
1314
|
+
this.sceneRenderTexture = this.device.createTexture({
|
|
1315
|
+
label: "scene render texture",
|
|
1316
|
+
size: [width, height],
|
|
1317
|
+
format: this.presentationFormat,
|
|
1318
|
+
usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING,
|
|
1319
|
+
})
|
|
1320
|
+
this.sceneRenderTextureView = this.sceneRenderTexture.createView()
|
|
1321
|
+
|
|
1322
|
+
// Create bloom textures (half resolution for performance)
|
|
1323
|
+
const bloomWidth = Math.floor(width / 2)
|
|
1324
|
+
const bloomHeight = Math.floor(height / 2)
|
|
1325
|
+
this.bloomExtractTexture = this.device.createTexture({
|
|
1326
|
+
label: "bloom extract",
|
|
1327
|
+
size: [bloomWidth, bloomHeight],
|
|
1328
|
+
format: this.presentationFormat,
|
|
1329
|
+
usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING,
|
|
1330
|
+
})
|
|
1331
|
+
this.bloomBlurTexture1 = this.device.createTexture({
|
|
1332
|
+
label: "bloom blur 1",
|
|
1333
|
+
size: [bloomWidth, bloomHeight],
|
|
1334
|
+
format: this.presentationFormat,
|
|
1335
|
+
usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING,
|
|
1336
|
+
})
|
|
1337
|
+
this.bloomBlurTexture2 = this.device.createTexture({
|
|
1338
|
+
label: "bloom blur 2",
|
|
1339
|
+
size: [bloomWidth, bloomHeight],
|
|
1340
|
+
format: this.presentationFormat,
|
|
1341
|
+
usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING,
|
|
1342
|
+
})
|
|
1343
|
+
|
|
1016
1344
|
const depthTextureView = this.depthTexture.createView()
|
|
1017
1345
|
|
|
1346
|
+
// Render scene to texture instead of directly to canvas
|
|
1018
1347
|
const colorAttachment: GPURenderPassColorAttachment =
|
|
1019
1348
|
this.sampleCount > 1
|
|
1020
1349
|
? {
|
|
1021
1350
|
view: this.multisampleTexture.createView(),
|
|
1022
|
-
resolveTarget: this.
|
|
1351
|
+
resolveTarget: this.sceneRenderTextureView,
|
|
1023
1352
|
clearValue: { r: 0, g: 0, b: 0, a: 0 },
|
|
1024
1353
|
loadOp: "clear",
|
|
1025
1354
|
storeOp: "store",
|
|
1026
1355
|
}
|
|
1027
1356
|
: {
|
|
1028
|
-
view: this.
|
|
1357
|
+
view: this.sceneRenderTextureView,
|
|
1029
1358
|
clearValue: { r: 0, g: 0, b: 0, a: 0 },
|
|
1030
1359
|
loadOp: "clear",
|
|
1031
1360
|
storeOp: "store",
|
|
@@ -1377,8 +1706,7 @@ export class Engine {
|
|
|
1377
1706
|
})
|
|
1378
1707
|
this.device.queue.writeBuffer(materialUniformBuffer, 0, materialUniformData)
|
|
1379
1708
|
|
|
1380
|
-
// Create bind groups using the shared bind group layout
|
|
1381
|
-
// All pipelines (main, eye, hair multiply, hair opaque) use the same shader and layout
|
|
1709
|
+
// Create bind groups using the shared bind group layout - All pipelines (main, eye, hair multiply, hair opaque) use the same shader and layout
|
|
1382
1710
|
const bindGroup = this.device.createBindGroup({
|
|
1383
1711
|
label: `material bind group: ${mat.name}`,
|
|
1384
1712
|
layout: this.hairBindGroupLayout,
|
|
@@ -1425,8 +1753,7 @@ export class Engine {
|
|
|
1425
1753
|
})
|
|
1426
1754
|
}
|
|
1427
1755
|
|
|
1428
|
-
// Outline for all materials (including transparent)
|
|
1429
|
-
// Edge flag is at bit 4 (0x10) in PMX format, not bit 0 (0x01)
|
|
1756
|
+
// Outline for all materials (including transparent) - Edge flag is at bit 4 (0x10) in PMX format, not bit 0 (0x01)
|
|
1430
1757
|
if ((mat.edgeFlag & 0x10) !== 0 && mat.edgeSize > 0) {
|
|
1431
1758
|
const materialUniformData = new Float32Array(8)
|
|
1432
1759
|
materialUniformData[0] = mat.edgeColor[0]
|
|
@@ -1560,8 +1887,9 @@ export class Engine {
|
|
|
1560
1887
|
|
|
1561
1888
|
this.drawCallCount = 0
|
|
1562
1889
|
|
|
1563
|
-
//
|
|
1564
|
-
this.drawOutlines(pass, false) // Opaque outlines
|
|
1890
|
+
// PASS 1: Opaque non-eye, non-hair (face, body, etc)
|
|
1891
|
+
// this.drawOutlines(pass, false) // Opaque outlines
|
|
1892
|
+
|
|
1565
1893
|
pass.setPipeline(this.pipeline)
|
|
1566
1894
|
for (const draw of this.opaqueNonEyeNonHairDraws) {
|
|
1567
1895
|
if (draw.count > 0) {
|
|
@@ -1571,7 +1899,7 @@ export class Engine {
|
|
|
1571
1899
|
}
|
|
1572
1900
|
}
|
|
1573
1901
|
|
|
1574
|
-
//
|
|
1902
|
+
// PASS 2: Eyes (writes stencil = 1)
|
|
1575
1903
|
pass.setPipeline(this.eyePipeline)
|
|
1576
1904
|
pass.setStencilReference(1) // Set stencil reference value to 1
|
|
1577
1905
|
for (const draw of this.eyeDraws) {
|
|
@@ -1582,8 +1910,7 @@ export class Engine {
|
|
|
1582
1910
|
}
|
|
1583
1911
|
}
|
|
1584
1912
|
|
|
1585
|
-
//
|
|
1586
|
-
// Draw hair geometry first to establish depth
|
|
1913
|
+
// PASS 3a: Hair over eyes (stencil == 1, multiply blend) - Draw hair geometry first to establish depth
|
|
1587
1914
|
pass.setPipeline(this.hairMultiplyPipeline)
|
|
1588
1915
|
pass.setStencilReference(1) // Check against stencil value 1
|
|
1589
1916
|
for (const draw of this.hairDraws) {
|
|
@@ -1594,9 +1921,7 @@ export class Engine {
|
|
|
1594
1921
|
}
|
|
1595
1922
|
}
|
|
1596
1923
|
|
|
1597
|
-
//
|
|
1598
|
-
// Use depth compare "less-equal" with the hair depth to only draw outline where hair exists
|
|
1599
|
-
// The outline is expanded outward, so we need to ensure it only appears near the hair edge
|
|
1924
|
+
// PASS 3a.5: Hair outlines over eyes (stencil == 1, depth test to only draw near hair)
|
|
1600
1925
|
pass.setPipeline(this.hairOutlineOverEyesPipeline)
|
|
1601
1926
|
pass.setStencilReference(1) // Check against stencil value 1 (with equal test)
|
|
1602
1927
|
for (const draw of this.hairOutlineDraws) {
|
|
@@ -1606,7 +1931,7 @@ export class Engine {
|
|
|
1606
1931
|
}
|
|
1607
1932
|
}
|
|
1608
1933
|
|
|
1609
|
-
//
|
|
1934
|
+
// PASS 3b: Hair over non-eyes (stencil != 1, opaque)
|
|
1610
1935
|
pass.setPipeline(this.hairOpaquePipeline)
|
|
1611
1936
|
pass.setStencilReference(1) // Check against stencil value 1 (with not-equal test)
|
|
1612
1937
|
for (const draw of this.hairDraws) {
|
|
@@ -1617,8 +1942,7 @@ export class Engine {
|
|
|
1617
1942
|
}
|
|
1618
1943
|
}
|
|
1619
1944
|
|
|
1620
|
-
//
|
|
1621
|
-
// Draw hair outlines after hair geometry, so they only appear where hair exists
|
|
1945
|
+
// PASS 3b.5: Hair outlines over non-eyes (stencil != 1) - Draw hair outlines after hair geometry, so they only appear where hair exists
|
|
1622
1946
|
pass.setPipeline(this.hairOutlinePipeline)
|
|
1623
1947
|
pass.setStencilReference(1) // Check against stencil value 1 (with not-equal test)
|
|
1624
1948
|
for (const draw of this.hairOutlineDraws) {
|
|
@@ -1628,7 +1952,9 @@ export class Engine {
|
|
|
1628
1952
|
}
|
|
1629
1953
|
}
|
|
1630
1954
|
|
|
1631
|
-
|
|
1955
|
+
this.drawOutlines(pass, false) // Opaque outlines
|
|
1956
|
+
|
|
1957
|
+
// PASS 4: Transparent non-eye, non-hair
|
|
1632
1958
|
pass.setPipeline(this.pipeline)
|
|
1633
1959
|
for (const draw of this.transparentNonEyeNonHairDraws) {
|
|
1634
1960
|
if (draw.count > 0) {
|
|
@@ -1642,10 +1968,156 @@ export class Engine {
|
|
|
1642
1968
|
|
|
1643
1969
|
pass.end()
|
|
1644
1970
|
this.device.queue.submit([encoder.finish()])
|
|
1971
|
+
|
|
1972
|
+
// Apply bloom post-processing
|
|
1973
|
+
this.applyBloom()
|
|
1974
|
+
|
|
1645
1975
|
this.updateStats(performance.now() - currentTime)
|
|
1646
1976
|
}
|
|
1647
1977
|
}
|
|
1648
1978
|
|
|
1979
|
+
// Apply bloom post-processing
|
|
1980
|
+
private applyBloom() {
|
|
1981
|
+
if (!this.sceneRenderTexture || !this.bloomExtractTexture) {
|
|
1982
|
+
return
|
|
1983
|
+
}
|
|
1984
|
+
|
|
1985
|
+
// Update bloom parameters
|
|
1986
|
+
const thresholdData = new Float32Array(8)
|
|
1987
|
+
thresholdData[0] = this.bloomThreshold
|
|
1988
|
+
this.device.queue.writeBuffer(this.bloomThresholdBuffer, 0, thresholdData)
|
|
1989
|
+
|
|
1990
|
+
const intensityData = new Float32Array(8)
|
|
1991
|
+
intensityData[0] = this.bloomIntensity
|
|
1992
|
+
this.device.queue.writeBuffer(this.bloomIntensityBuffer, 0, intensityData)
|
|
1993
|
+
|
|
1994
|
+
const encoder = this.device.createCommandEncoder()
|
|
1995
|
+
const width = this.canvas.width
|
|
1996
|
+
const height = this.canvas.height
|
|
1997
|
+
const bloomWidth = Math.floor(width / 2)
|
|
1998
|
+
const bloomHeight = Math.floor(height / 2)
|
|
1999
|
+
|
|
2000
|
+
// Pass 1: Extract bright areas (downsample to half resolution)
|
|
2001
|
+
const extractPass = encoder.beginRenderPass({
|
|
2002
|
+
label: "bloom extract",
|
|
2003
|
+
colorAttachments: [
|
|
2004
|
+
{
|
|
2005
|
+
view: this.bloomExtractTexture.createView(),
|
|
2006
|
+
clearValue: { r: 0, g: 0, b: 0, a: 0 },
|
|
2007
|
+
loadOp: "clear",
|
|
2008
|
+
storeOp: "store",
|
|
2009
|
+
},
|
|
2010
|
+
],
|
|
2011
|
+
})
|
|
2012
|
+
|
|
2013
|
+
const extractBindGroup = this.device.createBindGroup({
|
|
2014
|
+
layout: this.bloomExtractPipeline.getBindGroupLayout(0),
|
|
2015
|
+
entries: [
|
|
2016
|
+
{ binding: 0, resource: this.sceneRenderTexture.createView() },
|
|
2017
|
+
{ binding: 1, resource: this.linearSampler },
|
|
2018
|
+
{ binding: 2, resource: { buffer: this.bloomThresholdBuffer } },
|
|
2019
|
+
],
|
|
2020
|
+
})
|
|
2021
|
+
|
|
2022
|
+
extractPass.setPipeline(this.bloomExtractPipeline)
|
|
2023
|
+
extractPass.setBindGroup(0, extractBindGroup)
|
|
2024
|
+
extractPass.draw(6, 1, 0, 0)
|
|
2025
|
+
extractPass.end()
|
|
2026
|
+
|
|
2027
|
+
// Pass 2: Horizontal blur
|
|
2028
|
+
const hBlurData = new Float32Array(4) // vec2f + padding = 4 floats
|
|
2029
|
+
hBlurData[0] = 1.0
|
|
2030
|
+
hBlurData[1] = 0.0
|
|
2031
|
+
this.device.queue.writeBuffer(this.blurDirectionBuffer, 0, hBlurData)
|
|
2032
|
+
const blurHPass = encoder.beginRenderPass({
|
|
2033
|
+
label: "bloom blur horizontal",
|
|
2034
|
+
colorAttachments: [
|
|
2035
|
+
{
|
|
2036
|
+
view: this.bloomBlurTexture1.createView(),
|
|
2037
|
+
clearValue: { r: 0, g: 0, b: 0, a: 0 },
|
|
2038
|
+
loadOp: "clear",
|
|
2039
|
+
storeOp: "store",
|
|
2040
|
+
},
|
|
2041
|
+
],
|
|
2042
|
+
})
|
|
2043
|
+
|
|
2044
|
+
const blurHBindGroup = this.device.createBindGroup({
|
|
2045
|
+
layout: this.bloomBlurPipeline.getBindGroupLayout(0),
|
|
2046
|
+
entries: [
|
|
2047
|
+
{ binding: 0, resource: this.bloomExtractTexture.createView() },
|
|
2048
|
+
{ binding: 1, resource: this.linearSampler },
|
|
2049
|
+
{ binding: 2, resource: { buffer: this.blurDirectionBuffer } },
|
|
2050
|
+
],
|
|
2051
|
+
})
|
|
2052
|
+
|
|
2053
|
+
blurHPass.setPipeline(this.bloomBlurPipeline)
|
|
2054
|
+
blurHPass.setBindGroup(0, blurHBindGroup)
|
|
2055
|
+
blurHPass.draw(6, 1, 0, 0)
|
|
2056
|
+
blurHPass.end()
|
|
2057
|
+
|
|
2058
|
+
// Pass 3: Vertical blur
|
|
2059
|
+
const vBlurData = new Float32Array(4) // vec2f + padding = 4 floats
|
|
2060
|
+
vBlurData[0] = 0.0
|
|
2061
|
+
vBlurData[1] = 1.0
|
|
2062
|
+
this.device.queue.writeBuffer(this.blurDirectionBuffer, 0, vBlurData)
|
|
2063
|
+
const blurVPass = encoder.beginRenderPass({
|
|
2064
|
+
label: "bloom blur vertical",
|
|
2065
|
+
colorAttachments: [
|
|
2066
|
+
{
|
|
2067
|
+
view: this.bloomBlurTexture2.createView(),
|
|
2068
|
+
clearValue: { r: 0, g: 0, b: 0, a: 0 },
|
|
2069
|
+
loadOp: "clear",
|
|
2070
|
+
storeOp: "store",
|
|
2071
|
+
},
|
|
2072
|
+
],
|
|
2073
|
+
})
|
|
2074
|
+
|
|
2075
|
+
const blurVBindGroup = this.device.createBindGroup({
|
|
2076
|
+
layout: this.bloomBlurPipeline.getBindGroupLayout(0),
|
|
2077
|
+
entries: [
|
|
2078
|
+
{ binding: 0, resource: this.bloomBlurTexture1.createView() },
|
|
2079
|
+
{ binding: 1, resource: this.linearSampler },
|
|
2080
|
+
{ binding: 2, resource: { buffer: this.blurDirectionBuffer } },
|
|
2081
|
+
],
|
|
2082
|
+
})
|
|
2083
|
+
|
|
2084
|
+
blurVPass.setPipeline(this.bloomBlurPipeline)
|
|
2085
|
+
blurVPass.setBindGroup(0, blurVBindGroup)
|
|
2086
|
+
blurVPass.draw(6, 1, 0, 0)
|
|
2087
|
+
blurVPass.end()
|
|
2088
|
+
|
|
2089
|
+
// Pass 4: Compose scene + bloom to canvas
|
|
2090
|
+
const composePass = encoder.beginRenderPass({
|
|
2091
|
+
label: "bloom compose",
|
|
2092
|
+
colorAttachments: [
|
|
2093
|
+
{
|
|
2094
|
+
view: this.context.getCurrentTexture().createView(),
|
|
2095
|
+
clearValue: { r: 0, g: 0, b: 0, a: 0 },
|
|
2096
|
+
loadOp: "clear",
|
|
2097
|
+
storeOp: "store",
|
|
2098
|
+
},
|
|
2099
|
+
],
|
|
2100
|
+
})
|
|
2101
|
+
|
|
2102
|
+
const composeBindGroup = this.device.createBindGroup({
|
|
2103
|
+
layout: this.bloomComposePipeline.getBindGroupLayout(0),
|
|
2104
|
+
entries: [
|
|
2105
|
+
{ binding: 0, resource: this.sceneRenderTexture.createView() },
|
|
2106
|
+
{ binding: 1, resource: this.linearSampler },
|
|
2107
|
+
{ binding: 2, resource: this.bloomBlurTexture2.createView() },
|
|
2108
|
+
{ binding: 3, resource: this.linearSampler },
|
|
2109
|
+
{ binding: 4, resource: { buffer: this.bloomIntensityBuffer } },
|
|
2110
|
+
],
|
|
2111
|
+
})
|
|
2112
|
+
|
|
2113
|
+
composePass.setPipeline(this.bloomComposePipeline)
|
|
2114
|
+
composePass.setBindGroup(0, composeBindGroup)
|
|
2115
|
+
composePass.draw(6, 1, 0, 0)
|
|
2116
|
+
composePass.end()
|
|
2117
|
+
|
|
2118
|
+
this.device.queue.submit([encoder.finish()])
|
|
2119
|
+
}
|
|
2120
|
+
|
|
1649
2121
|
// Update camera uniform buffer each frame
|
|
1650
2122
|
private updateCameraUniforms() {
|
|
1651
2123
|
const viewMatrix = this.camera.getViewMatrix()
|
|
@@ -1663,9 +2135,11 @@ export class Engine {
|
|
|
1663
2135
|
private updateRenderTarget() {
|
|
1664
2136
|
const colorAttachment = (this.renderPassDescriptor.colorAttachments as GPURenderPassColorAttachment[])[0]
|
|
1665
2137
|
if (this.sampleCount > 1) {
|
|
1666
|
-
|
|
2138
|
+
// Resolve to scene render texture for post-processing
|
|
2139
|
+
colorAttachment.resolveTarget = this.sceneRenderTextureView
|
|
1667
2140
|
} else {
|
|
1668
|
-
|
|
2141
|
+
// Render directly to scene render texture
|
|
2142
|
+
colorAttachment.view = this.sceneRenderTextureView
|
|
1669
2143
|
}
|
|
1670
2144
|
}
|
|
1671
2145
|
|