reze-engine 0.8.0 → 0.8.1
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.d.ts +24 -32
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +374 -486
- package/dist/model.d.ts +9 -3
- package/dist/model.d.ts.map +1 -1
- package/dist/model.js +20 -24
- package/package.json +1 -1
- package/src/engine.ts +393 -592
- package/src/model.ts +28 -28
package/src/engine.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { Camera } from "./camera"
|
|
2
2
|
import { Mat4, Quat, Vec3 } from "./math"
|
|
3
3
|
import { Model } from "./model"
|
|
4
|
+
import { Physics } from "./physics"
|
|
4
5
|
|
|
5
|
-
export type RaycastCallback = (material: string | null, screenX: number, screenY: number) => void
|
|
6
|
+
export type RaycastCallback = (modelName: string, material: string | null, screenX: number, screenY: number) => void
|
|
6
7
|
|
|
7
8
|
export type EngineOptions = {
|
|
8
9
|
ambientColor?: Vec3
|
|
@@ -13,9 +14,6 @@ export type EngineOptions = {
|
|
|
13
14
|
cameraTarget?: Vec3
|
|
14
15
|
cameraFov?: number
|
|
15
16
|
onRaycast?: RaycastCallback
|
|
16
|
-
disableIK?: boolean
|
|
17
|
-
disablePhysics?: boolean
|
|
18
|
-
// multisampleCount: 1 | 4 (default 4)
|
|
19
17
|
multisampleCount?: 1 | 4
|
|
20
18
|
}
|
|
21
19
|
|
|
@@ -30,8 +28,6 @@ export const DEFAULT_ENGINE_OPTIONS: RequiredEngineOptions = {
|
|
|
30
28
|
cameraTarget: new Vec3(0, 12.5, 0),
|
|
31
29
|
cameraFov: Math.PI / 4,
|
|
32
30
|
onRaycast: undefined,
|
|
33
|
-
disableIK: false,
|
|
34
|
-
disablePhysics: false,
|
|
35
31
|
multisampleCount: 4,
|
|
36
32
|
}
|
|
37
33
|
|
|
@@ -60,6 +56,23 @@ interface DrawCall {
|
|
|
60
56
|
materialName: string
|
|
61
57
|
}
|
|
62
58
|
|
|
59
|
+
interface ModelInstance {
|
|
60
|
+
name: string
|
|
61
|
+
model: Model
|
|
62
|
+
basePath: string
|
|
63
|
+
vertexBuffer: GPUBuffer
|
|
64
|
+
indexBuffer: GPUBuffer
|
|
65
|
+
jointsBuffer: GPUBuffer
|
|
66
|
+
weightsBuffer: GPUBuffer
|
|
67
|
+
skinMatrixBuffer: GPUBuffer
|
|
68
|
+
drawCalls: DrawCall[]
|
|
69
|
+
shadowDrawCalls: DrawCall[]
|
|
70
|
+
shadowBindGroup: GPUBindGroup
|
|
71
|
+
hiddenMaterials: Set<string>
|
|
72
|
+
physics: Physics | null
|
|
73
|
+
vertexBufferNeedsUpdate: boolean
|
|
74
|
+
}
|
|
75
|
+
|
|
63
76
|
export class Engine {
|
|
64
77
|
private static instance: Engine | null = null
|
|
65
78
|
|
|
@@ -83,8 +96,6 @@ export class Engine {
|
|
|
83
96
|
private lightUniformBuffer!: GPUBuffer
|
|
84
97
|
private lightData = new Float32Array(64)
|
|
85
98
|
private lightCount = 0
|
|
86
|
-
private vertexBuffer!: GPUBuffer
|
|
87
|
-
private indexBuffer?: GPUBuffer
|
|
88
99
|
private resizeObserver: ResizeObserver | null = null
|
|
89
100
|
private depthTexture!: GPUTexture
|
|
90
101
|
// Material rendering pipelines
|
|
@@ -102,10 +113,6 @@ export class Engine {
|
|
|
102
113
|
private hairOutlinePipeline!: GPURenderPipeline
|
|
103
114
|
private mainBindGroupLayout!: GPUBindGroupLayout
|
|
104
115
|
private outlineBindGroupLayout!: GPUBindGroupLayout
|
|
105
|
-
private jointsBuffer!: GPUBuffer
|
|
106
|
-
private weightsBuffer!: GPUBuffer
|
|
107
|
-
private skinMatrixBuffer?: GPUBuffer
|
|
108
|
-
private inverseBindMatrixBuffer?: GPUBuffer
|
|
109
116
|
private multisampleTexture!: GPUTexture
|
|
110
117
|
private sampleCount: 1 | 4 = 4
|
|
111
118
|
private renderPassDescriptor!: GPURenderPassDescriptor
|
|
@@ -132,7 +139,6 @@ export class Engine {
|
|
|
132
139
|
private shadowMapTexture?: GPUTexture
|
|
133
140
|
private shadowMapDepthView?: GPUTextureView
|
|
134
141
|
private shadowDepthPipeline!: GPURenderPipeline
|
|
135
|
-
private shadowBindGroup?: GPUBindGroup
|
|
136
142
|
private shadowLightVPBuffer!: GPUBuffer
|
|
137
143
|
private shadowLightVPMatrix = new Float32Array(16)
|
|
138
144
|
private groundShadowPipeline!: GPURenderPipeline
|
|
@@ -140,33 +146,21 @@ export class Engine {
|
|
|
140
146
|
private groundShadowBindGroup?: GPUBindGroup
|
|
141
147
|
private shadowComparisonSampler!: GPUSampler
|
|
142
148
|
private groundShadowMaterialBuffer?: GPUBuffer
|
|
143
|
-
private
|
|
149
|
+
private groundDrawCall: DrawCall | null = null
|
|
144
150
|
private shadowVPLightX = Number.NaN
|
|
145
151
|
private shadowVPLightY = Number.NaN
|
|
146
152
|
private shadowVPLightZ = Number.NaN
|
|
147
153
|
|
|
148
|
-
// Raycasting
|
|
149
154
|
private onRaycast?: RaycastCallback
|
|
150
|
-
private cachedSkinnedVertices?: Float32Array
|
|
151
|
-
private cachedSkinMatricesVersion = -1
|
|
152
|
-
private skinMatricesVersion = 0
|
|
153
155
|
// Double-tap detection
|
|
154
156
|
private lastTouchTime = 0
|
|
155
157
|
private readonly DOUBLE_TAP_DELAY = 300 // ms
|
|
156
158
|
|
|
157
|
-
|
|
158
|
-
private _disableIK = false
|
|
159
|
-
private _disablePhysics = false
|
|
160
|
-
|
|
161
|
-
private currentModel: Model | null = null
|
|
162
|
-
private modelDir: string = ""
|
|
159
|
+
private modelInstances = new Map<string, ModelInstance>()
|
|
163
160
|
private materialSampler!: GPUSampler
|
|
164
161
|
private textureCache = new Map<string, GPUTexture>()
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
private drawCalls: DrawCall[] = []
|
|
168
|
-
// Material visibility tracking
|
|
169
|
-
private hiddenMaterials = new Set<string>()
|
|
162
|
+
/** Reusable buffer for raycast skinning to avoid per-instance allocations (Three.js/Babylon.js style). */
|
|
163
|
+
private raycastVertexBuffer: Float32Array | null = null
|
|
170
164
|
|
|
171
165
|
private lastFpsUpdate = performance.now()
|
|
172
166
|
private framesSinceLastUpdate = 0
|
|
@@ -193,8 +187,6 @@ export class Engine {
|
|
|
193
187
|
this.cameraTarget = options.cameraTarget ?? DEFAULT_ENGINE_OPTIONS.cameraTarget
|
|
194
188
|
this.cameraFov = options.cameraFov ?? DEFAULT_ENGINE_OPTIONS.cameraFov
|
|
195
189
|
this.onRaycast = options.onRaycast
|
|
196
|
-
this._disableIK = options.disableIK ?? DEFAULT_ENGINE_OPTIONS.disableIK
|
|
197
|
-
this._disablePhysics = options.disablePhysics ?? DEFAULT_ENGINE_OPTIONS.disablePhysics
|
|
198
190
|
}
|
|
199
191
|
}
|
|
200
192
|
|
|
@@ -1127,7 +1119,6 @@ export class Engine {
|
|
|
1127
1119
|
...options,
|
|
1128
1120
|
}
|
|
1129
1121
|
this.groundMode = opts.mode
|
|
1130
|
-
this.drawCalls = this.drawCalls.filter((d) => d.type !== "ground")
|
|
1131
1122
|
this.createGroundGeometry(opts.width, opts.height)
|
|
1132
1123
|
if (opts.mode === "reflection") {
|
|
1133
1124
|
this.createGroundMaterialBuffer(opts.diffuseColor, opts.reflectionLevel, opts.fadeStart, opts.fadeEnd)
|
|
@@ -1136,13 +1127,13 @@ export class Engine {
|
|
|
1136
1127
|
this.createShadowGroundResources(opts.shadowMapSize, opts.diffuseColor, opts.fadeStart, opts.fadeEnd, opts.shadowStrength)
|
|
1137
1128
|
}
|
|
1138
1129
|
this.groundHasReflections = true
|
|
1139
|
-
this.
|
|
1130
|
+
this.groundDrawCall = {
|
|
1140
1131
|
type: "ground",
|
|
1141
1132
|
count: 6,
|
|
1142
1133
|
firstIndex: 0,
|
|
1143
1134
|
bindGroup: (opts.mode === "reflection" ? this.groundReflectionBindGroup : this.groundShadowBindGroup)!,
|
|
1144
1135
|
materialName: "Ground",
|
|
1145
|
-
}
|
|
1136
|
+
}
|
|
1146
1137
|
}
|
|
1147
1138
|
|
|
1148
1139
|
private updateLightBuffer() {
|
|
@@ -1179,7 +1170,7 @@ export class Engine {
|
|
|
1179
1170
|
|
|
1180
1171
|
public dispose() {
|
|
1181
1172
|
this.stopRenderLoop()
|
|
1182
|
-
this.
|
|
1173
|
+
this.forEachInstance((inst) => inst.model.stopAnimation())
|
|
1183
1174
|
if (Engine.instance === this) Engine.instance = null
|
|
1184
1175
|
if (this.camera) this.camera.detachControl()
|
|
1185
1176
|
|
|
@@ -1195,150 +1186,201 @@ export class Engine {
|
|
|
1195
1186
|
}
|
|
1196
1187
|
}
|
|
1197
1188
|
|
|
1198
|
-
|
|
1199
|
-
|
|
1189
|
+
public async addModel(model: Model, pmxPath: string, name?: string): Promise<string> {
|
|
1190
|
+
const requested = name ?? model.name
|
|
1191
|
+
let key = requested
|
|
1192
|
+
let n = 1
|
|
1193
|
+
while (this.modelInstances.has(key)) {
|
|
1194
|
+
key = `${requested}_${n++}`
|
|
1195
|
+
}
|
|
1200
1196
|
const pathParts = pmxPath.split("/")
|
|
1201
1197
|
pathParts.pop()
|
|
1202
|
-
|
|
1203
|
-
this.
|
|
1204
|
-
|
|
1205
|
-
await this.setupModelBuffers(model)
|
|
1198
|
+
const basePath = pathParts.join("/") + "/"
|
|
1199
|
+
await this.setupModelInstance(key, model, basePath)
|
|
1200
|
+
return key
|
|
1206
1201
|
}
|
|
1207
1202
|
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
this.vertexBufferNeedsUpdate = true
|
|
1203
|
+
public async registerModel(model: Model, pmxPath: string): Promise<string> {
|
|
1204
|
+
return this.addModel(model, pmxPath)
|
|
1211
1205
|
}
|
|
1212
1206
|
|
|
1213
|
-
public
|
|
1214
|
-
|
|
1215
|
-
this.hiddenMaterials.delete(name)
|
|
1216
|
-
} else {
|
|
1217
|
-
this.hiddenMaterials.add(name)
|
|
1218
|
-
}
|
|
1207
|
+
public removeModel(name: string): void {
|
|
1208
|
+
this.modelInstances.delete(name)
|
|
1219
1209
|
}
|
|
1220
1210
|
|
|
1221
|
-
public
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1211
|
+
public getModelNames(): string[] {
|
|
1212
|
+
return Array.from(this.modelInstances.keys())
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
public getModel(name: string): Model | null {
|
|
1216
|
+
return this.modelInstances.get(name)?.model ?? null
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
public markVertexBufferDirty(modelNameOrModel?: string | Model): void {
|
|
1220
|
+
if (modelNameOrModel === undefined) return
|
|
1221
|
+
if (typeof modelNameOrModel === "string") {
|
|
1222
|
+
const inst = this.modelInstances.get(modelNameOrModel)
|
|
1223
|
+
if (inst) inst.vertexBufferNeedsUpdate = true
|
|
1224
|
+
return
|
|
1225
|
+
}
|
|
1226
|
+
for (const inst of this.modelInstances.values()) {
|
|
1227
|
+
if (inst.model === modelNameOrModel) {
|
|
1228
|
+
inst.vertexBufferNeedsUpdate = true
|
|
1229
|
+
return
|
|
1230
|
+
}
|
|
1226
1231
|
}
|
|
1227
1232
|
}
|
|
1228
1233
|
|
|
1229
|
-
public
|
|
1230
|
-
|
|
1234
|
+
public setMaterialVisible(modelName: string, materialName: string, visible: boolean): void {
|
|
1235
|
+
const inst = this.modelInstances.get(modelName)
|
|
1236
|
+
if (!inst) return
|
|
1237
|
+
if (visible) inst.hiddenMaterials.delete(materialName)
|
|
1238
|
+
else inst.hiddenMaterials.add(materialName)
|
|
1231
1239
|
}
|
|
1232
1240
|
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
return
|
|
1241
|
+
public toggleMaterialVisible(modelName: string, materialName: string): void {
|
|
1242
|
+
const inst = this.modelInstances.get(modelName)
|
|
1243
|
+
if (!inst) return
|
|
1244
|
+
if (inst.hiddenMaterials.has(materialName)) inst.hiddenMaterials.delete(materialName)
|
|
1245
|
+
else inst.hiddenMaterials.add(materialName)
|
|
1236
1246
|
}
|
|
1237
1247
|
|
|
1238
|
-
public
|
|
1239
|
-
|
|
1240
|
-
|
|
1248
|
+
public isMaterialVisible(modelName: string, materialName: string): boolean {
|
|
1249
|
+
const inst = this.modelInstances.get(modelName)
|
|
1250
|
+
return inst ? !inst.hiddenMaterials.has(materialName) : false
|
|
1241
1251
|
}
|
|
1242
1252
|
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
return this._disablePhysics
|
|
1253
|
+
public setModelIKEnabled(modelName: string, enabled: boolean): void {
|
|
1254
|
+
this.modelInstances.get(modelName)?.model.setIKEnabled(enabled)
|
|
1246
1255
|
}
|
|
1247
1256
|
|
|
1248
|
-
public
|
|
1249
|
-
this.
|
|
1250
|
-
this.currentModel?.setPhysicsEnabled(!value)
|
|
1257
|
+
public setModelPhysicsEnabled(modelName: string, enabled: boolean): void {
|
|
1258
|
+
this.modelInstances.get(modelName)?.model.setPhysicsEnabled(enabled)
|
|
1251
1259
|
}
|
|
1252
1260
|
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1261
|
+
public resetPhysics(): void {
|
|
1262
|
+
this.forEachInstance((inst) => {
|
|
1263
|
+
if (!inst.physics) return
|
|
1264
|
+
inst.model.computeWorldMatrices()
|
|
1265
|
+
inst.physics.reset(inst.model.getWorldMatrices(), inst.model.getBoneInverseBindMatrices())
|
|
1266
|
+
})
|
|
1258
1267
|
}
|
|
1259
1268
|
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1269
|
+
private instances(): IterableIterator<ModelInstance> {
|
|
1270
|
+
return this.modelInstances.values()
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
private forEachInstance(fn: (inst: ModelInstance) => void): void {
|
|
1274
|
+
for (const inst of this.instances()) fn(inst)
|
|
1275
|
+
}
|
|
1263
1276
|
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1277
|
+
private updateInstances(deltaTime: number): void {
|
|
1278
|
+
this.forEachInstance((inst) => {
|
|
1279
|
+
const verticesChanged = inst.model.update(deltaTime)
|
|
1280
|
+
if (verticesChanged) inst.vertexBufferNeedsUpdate = true
|
|
1281
|
+
if (inst.physics && inst.model.getPhysicsEnabled()) {
|
|
1282
|
+
inst.physics.step(
|
|
1283
|
+
deltaTime,
|
|
1284
|
+
inst.model.getWorldMatrices(),
|
|
1285
|
+
inst.model.getBoneInverseBindMatrices()
|
|
1286
|
+
)
|
|
1287
|
+
}
|
|
1288
|
+
if (inst.vertexBufferNeedsUpdate) this.updateVertexBuffer(inst)
|
|
1289
|
+
})
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
private updateVertexBuffer(inst: ModelInstance): void {
|
|
1293
|
+
const vertices = inst.model.getVertices()
|
|
1294
|
+
if (!vertices?.length) return
|
|
1295
|
+
this.device.queue.writeBuffer(inst.vertexBuffer, 0, vertices)
|
|
1296
|
+
inst.vertexBufferNeedsUpdate = false
|
|
1297
|
+
}
|
|
1267
1298
|
|
|
1299
|
+
private async setupModelInstance(name: string, model: Model, basePath: string): Promise<void> {
|
|
1268
1300
|
const vertices = model.getVertices()
|
|
1269
1301
|
const skinning = model.getSkinning()
|
|
1270
1302
|
const skeleton = model.getSkeleton()
|
|
1303
|
+
const boneCount = skeleton.bones.length
|
|
1304
|
+
const matrixSize = boneCount * 16 * 4
|
|
1271
1305
|
|
|
1272
|
-
|
|
1273
|
-
label:
|
|
1306
|
+
const vertexBuffer = this.device.createBuffer({
|
|
1307
|
+
label: `${name}: vertex buffer`,
|
|
1274
1308
|
size: vertices.byteLength,
|
|
1275
1309
|
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
|
|
1276
1310
|
})
|
|
1277
|
-
this.device.queue.writeBuffer(
|
|
1311
|
+
this.device.queue.writeBuffer(vertexBuffer, 0, vertices)
|
|
1278
1312
|
|
|
1279
|
-
|
|
1280
|
-
label:
|
|
1313
|
+
const jointsBuffer = this.device.createBuffer({
|
|
1314
|
+
label: `${name}: joints buffer`,
|
|
1281
1315
|
size: skinning.joints.byteLength,
|
|
1282
1316
|
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
|
|
1283
1317
|
})
|
|
1284
1318
|
this.device.queue.writeBuffer(
|
|
1285
|
-
|
|
1319
|
+
jointsBuffer,
|
|
1286
1320
|
0,
|
|
1287
1321
|
skinning.joints.buffer,
|
|
1288
1322
|
skinning.joints.byteOffset,
|
|
1289
1323
|
skinning.joints.byteLength
|
|
1290
1324
|
)
|
|
1291
1325
|
|
|
1292
|
-
|
|
1293
|
-
label:
|
|
1326
|
+
const weightsBuffer = this.device.createBuffer({
|
|
1327
|
+
label: `${name}: weights buffer`,
|
|
1294
1328
|
size: skinning.weights.byteLength,
|
|
1295
1329
|
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
|
|
1296
1330
|
})
|
|
1297
1331
|
this.device.queue.writeBuffer(
|
|
1298
|
-
|
|
1332
|
+
weightsBuffer,
|
|
1299
1333
|
0,
|
|
1300
1334
|
skinning.weights.buffer,
|
|
1301
1335
|
skinning.weights.byteOffset,
|
|
1302
1336
|
skinning.weights.byteLength
|
|
1303
1337
|
)
|
|
1304
1338
|
|
|
1305
|
-
const
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
this.skinMatrixBuffer = this.device.createBuffer({
|
|
1309
|
-
label: "skin matrices",
|
|
1339
|
+
const skinMatrixBuffer = this.device.createBuffer({
|
|
1340
|
+
label: `${name}: skin matrices`,
|
|
1310
1341
|
size: Math.max(256, matrixSize),
|
|
1311
1342
|
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
|
|
1312
1343
|
})
|
|
1313
1344
|
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1345
|
+
const indices = model.getIndices()
|
|
1346
|
+
if (!indices) throw new Error("Model has no index buffer")
|
|
1347
|
+
const indexBuffer = this.device.createBuffer({
|
|
1348
|
+
label: `${name}: index buffer`,
|
|
1349
|
+
size: indices.byteLength,
|
|
1350
|
+
usage: GPUBufferUsage.INDEX | GPUBufferUsage.COPY_DST,
|
|
1318
1351
|
})
|
|
1352
|
+
this.device.queue.writeBuffer(indexBuffer, 0, indices)
|
|
1319
1353
|
|
|
1320
|
-
const
|
|
1321
|
-
|
|
1322
|
-
this.inverseBindMatrixBuffer,
|
|
1323
|
-
0,
|
|
1324
|
-
invBindMatrices.buffer,
|
|
1325
|
-
invBindMatrices.byteOffset,
|
|
1326
|
-
invBindMatrices.byteLength
|
|
1327
|
-
)
|
|
1354
|
+
const rbs = model.getRigidbodies()
|
|
1355
|
+
const physics = rbs.length > 0 ? new Physics(rbs, model.getJoints()) : null
|
|
1328
1356
|
|
|
1329
|
-
const
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
} else {
|
|
1338
|
-
throw new Error("Model has no index buffer")
|
|
1339
|
-
}
|
|
1357
|
+
const shadowBindGroup = this.device.createBindGroup({
|
|
1358
|
+
label: `${name}: shadow bind`,
|
|
1359
|
+
layout: this.shadowDepthPipeline.getBindGroupLayout(0),
|
|
1360
|
+
entries: [
|
|
1361
|
+
{ binding: 0, resource: { buffer: this.shadowLightVPBuffer } },
|
|
1362
|
+
{ binding: 1, resource: { buffer: skinMatrixBuffer } },
|
|
1363
|
+
],
|
|
1364
|
+
})
|
|
1340
1365
|
|
|
1341
|
-
|
|
1366
|
+
const inst: ModelInstance = {
|
|
1367
|
+
name,
|
|
1368
|
+
model,
|
|
1369
|
+
basePath,
|
|
1370
|
+
vertexBuffer,
|
|
1371
|
+
indexBuffer,
|
|
1372
|
+
jointsBuffer,
|
|
1373
|
+
weightsBuffer,
|
|
1374
|
+
skinMatrixBuffer,
|
|
1375
|
+
drawCalls: [],
|
|
1376
|
+
shadowDrawCalls: [],
|
|
1377
|
+
shadowBindGroup,
|
|
1378
|
+
hiddenMaterials: new Set(),
|
|
1379
|
+
physics,
|
|
1380
|
+
vertexBufferNeedsUpdate: false,
|
|
1381
|
+
}
|
|
1382
|
+
await this.setupMaterialsForInstance(inst)
|
|
1383
|
+
this.modelInstances.set(name, inst)
|
|
1342
1384
|
}
|
|
1343
1385
|
|
|
1344
1386
|
private createGroundGeometry(width: number = 100, height: number = 100) {
|
|
@@ -1490,14 +1532,6 @@ export class Engine {
|
|
|
1490
1532
|
gb[7] = 0
|
|
1491
1533
|
this.groundShadowMaterialBuffer = this.device.createBuffer({ size: 64, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST })
|
|
1492
1534
|
this.device.queue.writeBuffer(this.groundShadowMaterialBuffer, 0, gb)
|
|
1493
|
-
this.shadowBindGroup = this.device.createBindGroup({
|
|
1494
|
-
label: "shadow bind",
|
|
1495
|
-
layout: this.shadowDepthPipeline.getBindGroupLayout(0),
|
|
1496
|
-
entries: [
|
|
1497
|
-
{ binding: 0, resource: { buffer: this.shadowLightVPBuffer } },
|
|
1498
|
-
{ binding: 1, resource: { buffer: this.skinMatrixBuffer! } },
|
|
1499
|
-
],
|
|
1500
|
-
})
|
|
1501
1535
|
this.groundShadowBindGroup = this.device.createBindGroup({
|
|
1502
1536
|
label: "ground shadow bind",
|
|
1503
1537
|
layout: this.groundShadowBindGroupLayout,
|
|
@@ -1535,27 +1569,20 @@ export class Engine {
|
|
|
1535
1569
|
this.device.queue.writeBuffer(this.shadowLightVPBuffer, 0, this.shadowLightVPMatrix)
|
|
1536
1570
|
}
|
|
1537
1571
|
|
|
1538
|
-
private async
|
|
1572
|
+
private async setupMaterialsForInstance(inst: ModelInstance): Promise<void> {
|
|
1573
|
+
const model = inst.model
|
|
1539
1574
|
const materials = model.getMaterials()
|
|
1540
|
-
if (materials.length === 0)
|
|
1541
|
-
throw new Error("Model has no materials")
|
|
1542
|
-
}
|
|
1543
|
-
|
|
1575
|
+
if (materials.length === 0) throw new Error("Model has no materials")
|
|
1544
1576
|
const textures = model.getTextures()
|
|
1577
|
+
const prefix = `${inst.name}: `
|
|
1545
1578
|
|
|
1546
1579
|
const loadTextureByIndex = async (texIndex: number): Promise<GPUTexture | null> => {
|
|
1547
|
-
if (texIndex < 0 || texIndex >= textures.length)
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
const path = this.modelDir + textures[texIndex].path
|
|
1552
|
-
const texture = await this.createTextureFromPath(path)
|
|
1553
|
-
return texture
|
|
1580
|
+
if (texIndex < 0 || texIndex >= textures.length) return null
|
|
1581
|
+
const path = inst.basePath + textures[texIndex].path
|
|
1582
|
+
return this.createTextureFromPath(path)
|
|
1554
1583
|
}
|
|
1555
1584
|
|
|
1556
|
-
this.drawCalls = []
|
|
1557
1585
|
let currentIndexOffset = 0
|
|
1558
|
-
|
|
1559
1586
|
for (const mat of materials) {
|
|
1560
1587
|
const indexCount = mat.vertexCount
|
|
1561
1588
|
if (indexCount === 0) continue
|
|
@@ -1567,7 +1594,7 @@ export class Engine {
|
|
|
1567
1594
|
const isTransparent = materialAlpha < 1.0 - 0.001
|
|
1568
1595
|
|
|
1569
1596
|
const materialUniformBuffer = this.createMaterialUniformBuffer(
|
|
1570
|
-
mat.name,
|
|
1597
|
+
prefix + mat.name,
|
|
1571
1598
|
materialAlpha,
|
|
1572
1599
|
0.0,
|
|
1573
1600
|
[mat.diffuse[0], mat.diffuse[1], mat.diffuse[2]],
|
|
@@ -1576,34 +1603,26 @@ export class Engine {
|
|
|
1576
1603
|
mat.shininess
|
|
1577
1604
|
)
|
|
1578
1605
|
|
|
1579
|
-
// Create bind groups using the shared bind group layout - All pipelines (main, eye, hair multiply, hair opaque) use the same shader and layout
|
|
1580
1606
|
const bindGroup = this.device.createBindGroup({
|
|
1581
|
-
label:
|
|
1607
|
+
label: `${prefix}material: ${mat.name}`,
|
|
1582
1608
|
layout: this.mainBindGroupLayout,
|
|
1583
1609
|
entries: [
|
|
1584
1610
|
{ binding: 0, resource: { buffer: this.cameraUniformBuffer } },
|
|
1585
1611
|
{ binding: 1, resource: { buffer: this.lightUniformBuffer } },
|
|
1586
1612
|
{ binding: 2, resource: diffuseTexture.createView() },
|
|
1587
1613
|
{ binding: 3, resource: this.materialSampler },
|
|
1588
|
-
{ binding: 4, resource: { buffer:
|
|
1614
|
+
{ binding: 4, resource: { buffer: inst.skinMatrixBuffer } },
|
|
1589
1615
|
{ binding: 5, resource: { buffer: materialUniformBuffer } },
|
|
1590
1616
|
],
|
|
1591
1617
|
})
|
|
1592
1618
|
|
|
1593
1619
|
if (indexCount > 0) {
|
|
1594
1620
|
if (mat.isEye) {
|
|
1595
|
-
|
|
1596
|
-
type: "eye",
|
|
1597
|
-
count: indexCount,
|
|
1598
|
-
firstIndex: currentIndexOffset,
|
|
1599
|
-
bindGroup,
|
|
1600
|
-
materialName: mat.name,
|
|
1601
|
-
})
|
|
1621
|
+
inst.drawCalls.push({ type: "eye", count: indexCount, firstIndex: currentIndexOffset, bindGroup, materialName: mat.name })
|
|
1602
1622
|
} else if (mat.isHair) {
|
|
1603
|
-
// Hair materials: create separate bind groups for over-eyes vs over-non-eyes
|
|
1604
1623
|
const createHairBindGroup = (isOverEyes: boolean) => {
|
|
1605
|
-
const
|
|
1606
|
-
`${mat.name} (${isOverEyes ? "over eyes" : "over non-eyes"})`,
|
|
1624
|
+
const buf = this.createMaterialUniformBuffer(
|
|
1625
|
+
`${prefix}${mat.name} (${isOverEyes ? "over eyes" : "over non-eyes"})`,
|
|
1607
1626
|
materialAlpha,
|
|
1608
1627
|
isOverEyes ? 1.0 : 0.0,
|
|
1609
1628
|
[mat.diffuse[0], mat.diffuse[1], mat.diffuse[2]],
|
|
@@ -1611,108 +1630,67 @@ export class Engine {
|
|
|
1611
1630
|
mat.specular,
|
|
1612
1631
|
mat.shininess
|
|
1613
1632
|
)
|
|
1614
|
-
|
|
1615
1633
|
return this.device.createBindGroup({
|
|
1616
|
-
label:
|
|
1634
|
+
label: `${prefix}hair ${isOverEyes ? "over eyes" : "over non-eyes"}: ${mat.name}`,
|
|
1617
1635
|
layout: this.mainBindGroupLayout,
|
|
1618
1636
|
entries: [
|
|
1619
1637
|
{ binding: 0, resource: { buffer: this.cameraUniformBuffer } },
|
|
1620
1638
|
{ binding: 1, resource: { buffer: this.lightUniformBuffer } },
|
|
1621
1639
|
{ binding: 2, resource: diffuseTexture.createView() },
|
|
1622
1640
|
{ binding: 3, resource: this.materialSampler },
|
|
1623
|
-
{ binding: 4, resource: { buffer:
|
|
1624
|
-
{ binding: 5, resource: { buffer:
|
|
1641
|
+
{ binding: 4, resource: { buffer: inst.skinMatrixBuffer } },
|
|
1642
|
+
{ binding: 5, resource: { buffer: buf } },
|
|
1625
1643
|
],
|
|
1626
1644
|
})
|
|
1627
1645
|
}
|
|
1628
|
-
|
|
1629
|
-
const bindGroupOverEyes = createHairBindGroup(true)
|
|
1630
|
-
const bindGroupOverNonEyes = createHairBindGroup(false)
|
|
1631
|
-
|
|
1632
|
-
this.drawCalls.push({
|
|
1646
|
+
inst.drawCalls.push({
|
|
1633
1647
|
type: "hair-over-eyes",
|
|
1634
1648
|
count: indexCount,
|
|
1635
1649
|
firstIndex: currentIndexOffset,
|
|
1636
|
-
bindGroup:
|
|
1650
|
+
bindGroup: createHairBindGroup(true),
|
|
1637
1651
|
materialName: mat.name,
|
|
1638
1652
|
})
|
|
1639
|
-
|
|
1653
|
+
inst.drawCalls.push({
|
|
1640
1654
|
type: "hair-over-non-eyes",
|
|
1641
1655
|
count: indexCount,
|
|
1642
1656
|
firstIndex: currentIndexOffset,
|
|
1643
|
-
bindGroup:
|
|
1657
|
+
bindGroup: createHairBindGroup(false),
|
|
1644
1658
|
materialName: mat.name,
|
|
1645
1659
|
})
|
|
1646
1660
|
} else if (isTransparent) {
|
|
1647
|
-
|
|
1648
|
-
type: "transparent",
|
|
1649
|
-
count: indexCount,
|
|
1650
|
-
firstIndex: currentIndexOffset,
|
|
1651
|
-
bindGroup,
|
|
1652
|
-
materialName: mat.name,
|
|
1653
|
-
})
|
|
1661
|
+
inst.drawCalls.push({ type: "transparent", count: indexCount, firstIndex: currentIndexOffset, bindGroup, materialName: mat.name })
|
|
1654
1662
|
} else {
|
|
1655
|
-
|
|
1656
|
-
type: "opaque",
|
|
1657
|
-
count: indexCount,
|
|
1658
|
-
firstIndex: currentIndexOffset,
|
|
1659
|
-
bindGroup,
|
|
1660
|
-
materialName: mat.name,
|
|
1661
|
-
})
|
|
1663
|
+
inst.drawCalls.push({ type: "opaque", count: indexCount, firstIndex: currentIndexOffset, bindGroup, materialName: mat.name })
|
|
1662
1664
|
}
|
|
1663
1665
|
}
|
|
1664
1666
|
|
|
1665
|
-
// Edge flag is at bit 4 (0x10) in PMX format
|
|
1666
1667
|
if ((mat.edgeFlag & 0x10) !== 0 && mat.edgeSize > 0) {
|
|
1667
1668
|
const materialUniformData = new Float32Array([
|
|
1668
|
-
mat.edgeColor[0],
|
|
1669
|
-
mat.
|
|
1670
|
-
mat.edgeColor[2],
|
|
1671
|
-
mat.edgeColor[3],
|
|
1672
|
-
mat.edgeSize,
|
|
1673
|
-
0,
|
|
1674
|
-
0,
|
|
1675
|
-
0,
|
|
1669
|
+
mat.edgeColor[0], mat.edgeColor[1], mat.edgeColor[2], mat.edgeColor[3],
|
|
1670
|
+
mat.edgeSize, 0, 0, 0,
|
|
1676
1671
|
])
|
|
1677
|
-
const
|
|
1678
|
-
`outline material uniform: ${mat.name}`,
|
|
1679
|
-
materialUniformData
|
|
1680
|
-
)
|
|
1681
|
-
|
|
1672
|
+
const outlineUniformBuffer = this.createUniformBuffer(`${prefix}outline: ${mat.name}`, materialUniformData)
|
|
1682
1673
|
const outlineBindGroup = this.device.createBindGroup({
|
|
1683
|
-
label:
|
|
1674
|
+
label: `${prefix}outline: ${mat.name}`,
|
|
1684
1675
|
layout: this.outlineBindGroupLayout,
|
|
1685
1676
|
entries: [
|
|
1686
1677
|
{ binding: 0, resource: { buffer: this.cameraUniformBuffer } },
|
|
1687
|
-
{ binding: 1, resource: { buffer:
|
|
1688
|
-
{ binding: 2, resource: { buffer:
|
|
1678
|
+
{ binding: 1, resource: { buffer: outlineUniformBuffer } },
|
|
1679
|
+
{ binding: 2, resource: { buffer: inst.skinMatrixBuffer } },
|
|
1689
1680
|
],
|
|
1690
1681
|
})
|
|
1691
|
-
|
|
1692
1682
|
if (indexCount > 0) {
|
|
1693
|
-
const outlineType: DrawCallType = mat.isEye
|
|
1694
|
-
|
|
1695
|
-
: mat.isHair
|
|
1696
|
-
? "hair-outline"
|
|
1697
|
-
: isTransparent
|
|
1698
|
-
? "transparent-outline"
|
|
1699
|
-
: "opaque-outline"
|
|
1700
|
-
this.drawCalls.push({
|
|
1701
|
-
type: outlineType,
|
|
1702
|
-
count: indexCount,
|
|
1703
|
-
firstIndex: currentIndexOffset,
|
|
1704
|
-
bindGroup: outlineBindGroup,
|
|
1705
|
-
materialName: mat.name,
|
|
1706
|
-
})
|
|
1683
|
+
const outlineType: DrawCallType = mat.isEye ? "eye-outline" : mat.isHair ? "hair-outline" : isTransparent ? "transparent-outline" : "opaque-outline"
|
|
1684
|
+
inst.drawCalls.push({ type: outlineType, count: indexCount, firstIndex: currentIndexOffset, bindGroup: outlineBindGroup, materialName: mat.name })
|
|
1707
1685
|
}
|
|
1708
1686
|
}
|
|
1709
1687
|
|
|
1710
1688
|
currentIndexOffset += indexCount
|
|
1711
1689
|
}
|
|
1712
|
-
|
|
1713
|
-
for (const d of
|
|
1690
|
+
|
|
1691
|
+
for (const d of inst.drawCalls) {
|
|
1714
1692
|
if (d.type === "opaque" || d.type === "hair-over-eyes" || d.type === "hair-over-non-eyes")
|
|
1715
|
-
|
|
1693
|
+
inst.shadowDrawCalls.push(d)
|
|
1716
1694
|
}
|
|
1717
1695
|
}
|
|
1718
1696
|
|
|
@@ -1761,8 +1739,8 @@ export class Engine {
|
|
|
1761
1739
|
return buffer
|
|
1762
1740
|
}
|
|
1763
1741
|
|
|
1764
|
-
private shouldRenderDrawCall(drawCall: DrawCall): boolean {
|
|
1765
|
-
return !
|
|
1742
|
+
private shouldRenderDrawCall(inst: ModelInstance, drawCall: DrawCall): boolean {
|
|
1743
|
+
return !inst.hiddenMaterials.has(drawCall.materialName)
|
|
1766
1744
|
}
|
|
1767
1745
|
|
|
1768
1746
|
private async createTextureFromPath(path: string): Promise<GPUTexture | null> {
|
|
@@ -1800,11 +1778,10 @@ export class Engine {
|
|
|
1800
1778
|
}
|
|
1801
1779
|
|
|
1802
1780
|
// Helper: Render eyes with stencil writing (for post-alpha-eye effect)
|
|
1803
|
-
private renderEyes(pass: GPURenderPassEncoder,
|
|
1781
|
+
private renderEyes(pass: GPURenderPassEncoder, inst: ModelInstance, useReflectionPipeline = false) {
|
|
1804
1782
|
if (useReflectionPipeline) {
|
|
1805
|
-
// For reflections, use the basic reflection pipeline instead of specialized eye pipeline
|
|
1806
1783
|
pass.setPipeline(this.reflectionPipeline)
|
|
1807
|
-
for (const draw of
|
|
1784
|
+
for (const draw of inst.drawCalls) {
|
|
1808
1785
|
if (draw.type === "eye") {
|
|
1809
1786
|
pass.setBindGroup(0, draw.bindGroup)
|
|
1810
1787
|
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0)
|
|
@@ -1813,8 +1790,8 @@ export class Engine {
|
|
|
1813
1790
|
} else {
|
|
1814
1791
|
pass.setPipeline(this.eyePipeline)
|
|
1815
1792
|
pass.setStencilReference(this.STENCIL_EYE_VALUE)
|
|
1816
|
-
for (const draw of
|
|
1817
|
-
if (draw.type === "eye" && this.shouldRenderDrawCall(draw)) {
|
|
1793
|
+
for (const draw of inst.drawCalls) {
|
|
1794
|
+
if (draw.type === "eye" && this.shouldRenderDrawCall(inst, draw)) {
|
|
1818
1795
|
pass.setBindGroup(0, draw.bindGroup)
|
|
1819
1796
|
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0)
|
|
1820
1797
|
}
|
|
@@ -1823,17 +1800,13 @@ export class Engine {
|
|
|
1823
1800
|
}
|
|
1824
1801
|
|
|
1825
1802
|
private renderGround(pass: GPURenderPassEncoder) {
|
|
1826
|
-
if (!this.groundHasReflections || !this.groundVertexBuffer || !this.groundIndexBuffer) return
|
|
1803
|
+
if (!this.groundHasReflections || !this.groundVertexBuffer || !this.groundIndexBuffer || !this.groundDrawCall) return
|
|
1827
1804
|
if (this.groundMode === "reflection" && this.groundReflectionTexture) this.renderReflectionTexture()
|
|
1828
1805
|
pass.setPipeline(this.groundMode === "reflection" ? this.groundPipeline : this.groundShadowPipeline)
|
|
1829
1806
|
pass.setVertexBuffer(0, this.groundVertexBuffer)
|
|
1830
1807
|
pass.setIndexBuffer(this.groundIndexBuffer, "uint16")
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
pass.setBindGroup(0, draw.bindGroup)
|
|
1834
|
-
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0)
|
|
1835
|
-
}
|
|
1836
|
-
}
|
|
1808
|
+
pass.setBindGroup(0, this.groundDrawCall.bindGroup)
|
|
1809
|
+
pass.drawIndexed(this.groundDrawCall.count, 1, this.groundDrawCall.firstIndex, 0, 0)
|
|
1837
1810
|
}
|
|
1838
1811
|
|
|
1839
1812
|
private renderReflectionTexture() {
|
|
@@ -1866,55 +1839,16 @@ export class Engine {
|
|
|
1866
1839
|
}
|
|
1867
1840
|
|
|
1868
1841
|
const reflectionPass = reflectionEncoder.beginRenderPass(reflectionPassDescriptor)
|
|
1869
|
-
|
|
1870
|
-
if (this.currentModel) {
|
|
1871
|
-
reflectionPass.setVertexBuffer(0, this.vertexBuffer)
|
|
1872
|
-
reflectionPass.setVertexBuffer(1, this.jointsBuffer)
|
|
1873
|
-
reflectionPass.setVertexBuffer(2, this.weightsBuffer)
|
|
1874
|
-
reflectionPass.setIndexBuffer(this.indexBuffer!, "uint32")
|
|
1875
|
-
|
|
1876
|
-
this.writeMirrorTransformedSkinMatrices(mirrorMatrix)
|
|
1877
|
-
reflectionPass.setPipeline(this.reflectionPipeline)
|
|
1878
|
-
for (const draw of this.drawCalls) {
|
|
1879
|
-
if (draw.type === "opaque" && this.shouldRenderDrawCall(draw)) {
|
|
1880
|
-
reflectionPass.setBindGroup(0, draw.bindGroup)
|
|
1881
|
-
reflectionPass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0)
|
|
1882
|
-
}
|
|
1883
|
-
}
|
|
1884
|
-
|
|
1885
|
-
// Render eyes (using reflection pipeline)
|
|
1886
|
-
this.renderEyes(reflectionPass, true)
|
|
1887
|
-
|
|
1888
|
-
// Render hair (using reflection pipeline)
|
|
1889
|
-
this.renderHair(reflectionPass, true)
|
|
1890
|
-
|
|
1891
|
-
// Render transparent objects
|
|
1892
|
-
for (const draw of this.drawCalls) {
|
|
1893
|
-
if (draw.type === "transparent" && this.shouldRenderDrawCall(draw)) {
|
|
1894
|
-
reflectionPass.setBindGroup(0, draw.bindGroup)
|
|
1895
|
-
reflectionPass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0)
|
|
1896
|
-
}
|
|
1897
|
-
}
|
|
1898
|
-
|
|
1899
|
-
this.drawOutlines(reflectionPass, true, true)
|
|
1900
|
-
}
|
|
1901
|
-
|
|
1842
|
+
this.forEachInstance((inst) => this.renderOneModel(reflectionPass, inst, true, mirrorMatrix))
|
|
1902
1843
|
reflectionPass.end()
|
|
1903
|
-
|
|
1904
|
-
// Submit reflection rendering commands
|
|
1905
|
-
const reflectionCommandBuffer = reflectionEncoder.finish()
|
|
1906
|
-
this.device.queue.submit([reflectionCommandBuffer])
|
|
1907
|
-
|
|
1908
|
-
// Restore original skin matrices
|
|
1844
|
+
this.device.queue.submit([reflectionEncoder.finish()])
|
|
1909
1845
|
this.updateSkinMatrices()
|
|
1910
1846
|
}
|
|
1911
1847
|
|
|
1912
|
-
|
|
1913
|
-
private renderHair(pass: GPURenderPassEncoder, useReflectionPipeline: boolean = false) {
|
|
1848
|
+
private renderHair(pass: GPURenderPassEncoder, inst: ModelInstance, useReflectionPipeline = false) {
|
|
1914
1849
|
if (useReflectionPipeline) {
|
|
1915
|
-
// For reflections, use the basic reflection pipeline for all hair
|
|
1916
1850
|
pass.setPipeline(this.reflectionPipeline)
|
|
1917
|
-
for (const draw of
|
|
1851
|
+
for (const draw of inst.drawCalls) {
|
|
1918
1852
|
if (draw.type === "hair-over-eyes" || draw.type === "hair-over-non-eyes") {
|
|
1919
1853
|
pass.setBindGroup(0, draw.bindGroup)
|
|
1920
1854
|
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0)
|
|
@@ -1923,22 +1857,20 @@ export class Engine {
|
|
|
1923
1857
|
return
|
|
1924
1858
|
}
|
|
1925
1859
|
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
(d) => (d.type === "hair-over-eyes" || d.type === "hair-over-non-eyes") && this.shouldRenderDrawCall(d)
|
|
1860
|
+
const hasHair = inst.drawCalls.some(
|
|
1861
|
+
(d) => (d.type === "hair-over-eyes" || d.type === "hair-over-non-eyes") && this.shouldRenderDrawCall(inst, d)
|
|
1929
1862
|
)
|
|
1930
1863
|
if (hasHair) {
|
|
1931
1864
|
pass.setPipeline(this.hairDepthPipeline)
|
|
1932
|
-
for (const draw of
|
|
1933
|
-
if ((draw.type === "hair-over-eyes" || draw.type === "hair-over-non-eyes") && this.shouldRenderDrawCall(draw)) {
|
|
1865
|
+
for (const draw of inst.drawCalls) {
|
|
1866
|
+
if ((draw.type === "hair-over-eyes" || draw.type === "hair-over-non-eyes") && this.shouldRenderDrawCall(inst, draw)) {
|
|
1934
1867
|
pass.setBindGroup(0, draw.bindGroup)
|
|
1935
1868
|
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0)
|
|
1936
1869
|
}
|
|
1937
1870
|
}
|
|
1938
1871
|
}
|
|
1939
1872
|
|
|
1940
|
-
|
|
1941
|
-
const hairOverEyes = this.drawCalls.filter((d) => d.type === "hair-over-eyes" && this.shouldRenderDrawCall(d))
|
|
1873
|
+
const hairOverEyes = inst.drawCalls.filter((d) => d.type === "hair-over-eyes" && this.shouldRenderDrawCall(inst, d))
|
|
1942
1874
|
if (hairOverEyes.length > 0) {
|
|
1943
1875
|
pass.setPipeline(this.hairPipelineOverEyes)
|
|
1944
1876
|
pass.setStencilReference(this.STENCIL_EYE_VALUE)
|
|
@@ -1948,9 +1880,7 @@ export class Engine {
|
|
|
1948
1880
|
}
|
|
1949
1881
|
}
|
|
1950
1882
|
|
|
1951
|
-
const hairOverNonEyes =
|
|
1952
|
-
(d) => d.type === "hair-over-non-eyes" && this.shouldRenderDrawCall(d)
|
|
1953
|
-
)
|
|
1883
|
+
const hairOverNonEyes = inst.drawCalls.filter((d) => d.type === "hair-over-non-eyes" && this.shouldRenderDrawCall(inst, d))
|
|
1954
1884
|
if (hairOverNonEyes.length > 0) {
|
|
1955
1885
|
pass.setPipeline(this.hairPipelineOverNonEyes)
|
|
1956
1886
|
pass.setStencilReference(this.STENCIL_EYE_VALUE)
|
|
@@ -1960,8 +1890,7 @@ export class Engine {
|
|
|
1960
1890
|
}
|
|
1961
1891
|
}
|
|
1962
1892
|
|
|
1963
|
-
|
|
1964
|
-
const hairOutlines = this.drawCalls.filter((d) => d.type === "hair-outline" && this.shouldRenderDrawCall(d))
|
|
1893
|
+
const hairOutlines = inst.drawCalls.filter((d) => d.type === "hair-outline" && this.shouldRenderDrawCall(inst, d))
|
|
1965
1894
|
if (hairOutlines.length > 0) {
|
|
1966
1895
|
pass.setPipeline(this.hairOutlinePipeline)
|
|
1967
1896
|
for (const draw of hairOutlines) {
|
|
@@ -1972,17 +1901,13 @@ export class Engine {
|
|
|
1972
1901
|
}
|
|
1973
1902
|
|
|
1974
1903
|
private handleCanvasDoubleClick = (event: MouseEvent) => {
|
|
1975
|
-
if (!this.onRaycast ||
|
|
1976
|
-
|
|
1904
|
+
if (!this.onRaycast || this.modelInstances.size === 0) return
|
|
1977
1905
|
const rect = this.canvas.getBoundingClientRect()
|
|
1978
|
-
|
|
1979
|
-
const y = event.clientY - rect.top
|
|
1980
|
-
|
|
1981
|
-
this.performRaycast(x, y)
|
|
1906
|
+
this.performRaycast(event.clientX - rect.left, event.clientY - rect.top)
|
|
1982
1907
|
}
|
|
1983
1908
|
|
|
1984
1909
|
private handleCanvasTouch = (event: TouchEvent) => {
|
|
1985
|
-
if (!this.onRaycast ||
|
|
1910
|
+
if (!this.onRaycast || this.modelInstances.size === 0) return
|
|
1986
1911
|
|
|
1987
1912
|
// Prevent default to avoid triggering mouse events
|
|
1988
1913
|
event.preventDefault()
|
|
@@ -2010,324 +1935,215 @@ export class Engine {
|
|
|
2010
1935
|
}
|
|
2011
1936
|
|
|
2012
1937
|
private performRaycast(screenX: number, screenY: number) {
|
|
2013
|
-
if (!this.
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
1938
|
+
if (!this.onRaycast || this.modelInstances.size === 0) {
|
|
1939
|
+
this.onRaycast?.("", null, screenX, screenY)
|
|
1940
|
+
return
|
|
1941
|
+
}
|
|
2017
1942
|
|
|
2018
|
-
// Get camera matrices
|
|
2019
1943
|
const viewMatrix = this.camera.getViewMatrix()
|
|
2020
1944
|
const projectionMatrix = this.camera.getProjectionMatrix()
|
|
2021
|
-
|
|
2022
|
-
// Convert screen coordinates to world space ray
|
|
2023
|
-
const canvas = this.canvas
|
|
2024
|
-
const rect = canvas.getBoundingClientRect()
|
|
2025
|
-
|
|
2026
|
-
// Convert to clip space (-1 to 1)
|
|
1945
|
+
const rect = this.canvas.getBoundingClientRect()
|
|
2027
1946
|
const clipX = (screenX / rect.width) * 2 - 1
|
|
2028
|
-
const clipY = 1 - (screenY / rect.height) * 2
|
|
2029
|
-
|
|
2030
|
-
// Create ray in clip space at near and far planes
|
|
2031
|
-
const clipNear = new Vec3(clipX, clipY, -1) // Near plane
|
|
2032
|
-
const clipFar = new Vec3(clipX, clipY, 1) // Far plane
|
|
2033
|
-
|
|
2034
|
-
// Transform to world space using inverse view-projection matrix
|
|
1947
|
+
const clipY = 1 - (screenY / rect.height) * 2
|
|
2035
1948
|
const viewProjMatrix = projectionMatrix.multiply(viewMatrix)
|
|
2036
1949
|
const inverseViewProj = viewProjMatrix.inverse()
|
|
2037
|
-
|
|
2038
|
-
// Transform point through 4x4 matrix with perspective division
|
|
2039
1950
|
const transformPoint = (matrix: Mat4, point: Vec3): Vec3 => {
|
|
2040
1951
|
const m = matrix.values
|
|
2041
|
-
const x = point.x,
|
|
2042
|
-
y = point.y,
|
|
2043
|
-
z = point.z
|
|
2044
|
-
|
|
2045
|
-
// Compute transformed point (matrix * vec4(point, 1.0))
|
|
1952
|
+
const x = point.x, y = point.y, z = point.z
|
|
2046
1953
|
const result = new Vec3(
|
|
2047
1954
|
m[0] * x + m[4] * y + m[8] * z + m[12],
|
|
2048
1955
|
m[1] * x + m[5] * y + m[9] * z + m[13],
|
|
2049
1956
|
m[2] * x + m[6] * y + m[10] * z + m[14]
|
|
2050
1957
|
)
|
|
2051
|
-
|
|
2052
|
-
// Perspective division
|
|
2053
1958
|
const w = m[3] * x + m[7] * y + m[11] * z + m[15]
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
return result.scale(invW)
|
|
1959
|
+
return result.scale(w !== 0 ? 1 / w : 1)
|
|
2057
1960
|
}
|
|
2058
|
-
|
|
2059
|
-
const
|
|
2060
|
-
const worldFar = transformPoint(inverseViewProj, clipFar)
|
|
2061
|
-
|
|
2062
|
-
// Create ray from camera position through the clicked point
|
|
1961
|
+
const worldNear = transformPoint(inverseViewProj, new Vec3(clipX, clipY, -1))
|
|
1962
|
+
const worldFar = transformPoint(inverseViewProj, new Vec3(clipX, clipY, 1))
|
|
2063
1963
|
const rayOrigin = this.camera.getPosition()
|
|
2064
1964
|
const rayDirection = worldFar.subtract(worldNear).normalize()
|
|
2065
1965
|
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
this.onRaycast(null, screenX, screenY)
|
|
2074
|
-
}
|
|
2075
|
-
return
|
|
1966
|
+
const transformByMatrix = (matrix: Float32Array, offset: number, point: Vec3): Vec3 => {
|
|
1967
|
+
const m = matrix, x = point.x, y = point.y, z = point.z
|
|
1968
|
+
return new Vec3(
|
|
1969
|
+
m[offset + 0] * x + m[offset + 4] * y + m[offset + 8] * z + m[offset + 12],
|
|
1970
|
+
m[offset + 1] * x + m[offset + 5] * y + m[offset + 9] * z + m[offset + 13],
|
|
1971
|
+
m[offset + 2] * x + m[offset + 6] * y + m[offset + 10] * z + m[offset + 14]
|
|
1972
|
+
)
|
|
2076
1973
|
}
|
|
2077
1974
|
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
if (this.cachedSkinnedVertices && this.cachedSkinMatricesVersion === this.skinMatricesVersion) {
|
|
2081
|
-
vertices = this.cachedSkinnedVertices
|
|
2082
|
-
} else {
|
|
2083
|
-
// Apply current skinning transformations to get animated vertex positions
|
|
2084
|
-
vertices = new Float32Array(baseVertices.length)
|
|
2085
|
-
const skinMatrices = this.currentModel.getSkinMatrices()
|
|
2086
|
-
|
|
2087
|
-
// Helper function to transform point by 4x4 matrix
|
|
2088
|
-
const transformByMatrix = (matrix: Float32Array, offset: number, point: Vec3): Vec3 => {
|
|
2089
|
-
const m = matrix
|
|
2090
|
-
const x = point.x,
|
|
2091
|
-
y = point.y,
|
|
2092
|
-
z = point.z
|
|
2093
|
-
return new Vec3(
|
|
2094
|
-
m[offset + 0] * x + m[offset + 4] * y + m[offset + 8] * z + m[offset + 12],
|
|
2095
|
-
m[offset + 1] * x + m[offset + 5] * y + m[offset + 9] * z + m[offset + 13],
|
|
2096
|
-
m[offset + 2] * x + m[offset + 6] * y + m[offset + 10] * z + m[offset + 14]
|
|
2097
|
-
)
|
|
2098
|
-
}
|
|
1975
|
+
let closest: { modelName: string; materialName: string; distance: number } | null = null
|
|
1976
|
+
const maxDistance = 1000
|
|
2099
1977
|
|
|
1978
|
+
this.forEachInstance((inst) => {
|
|
1979
|
+
const model = inst.model
|
|
1980
|
+
const materials = model.getMaterials()
|
|
1981
|
+
if (materials.length === 0) return
|
|
1982
|
+
const baseVertices = model.getVertices()
|
|
1983
|
+
const indices = model.getIndices()
|
|
1984
|
+
const skinning = model.getSkinning()
|
|
1985
|
+
if (!baseVertices?.length || !indices || !skinning) return
|
|
1986
|
+
|
|
1987
|
+
const vertices = new Float32Array(baseVertices.length)
|
|
1988
|
+
const skinMatrices = model.getSkinMatrices()
|
|
2100
1989
|
for (let i = 0; i < baseVertices.length; i += 8) {
|
|
2101
|
-
const vertexIndex =
|
|
1990
|
+
const vertexIndex = i / 8
|
|
2102
1991
|
const position = new Vec3(baseVertices[i], baseVertices[i + 1], baseVertices[i + 2])
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
const
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
skinning.joints[vertexIndex * 4 + 2],
|
|
2109
|
-
skinning.joints[vertexIndex * 4 + 3],
|
|
2110
|
-
]
|
|
2111
|
-
|
|
2112
|
-
const weights = [
|
|
2113
|
-
skinning.weights[vertexIndex * 4],
|
|
2114
|
-
skinning.weights[vertexIndex * 4 + 1],
|
|
2115
|
-
skinning.weights[vertexIndex * 4 + 2],
|
|
2116
|
-
skinning.weights[vertexIndex * 4 + 3],
|
|
2117
|
-
]
|
|
2118
|
-
|
|
2119
|
-
// Normalize weights (same as shader)
|
|
2120
|
-
const weightSum = weights[0] + weights[1] + weights[2] + weights[3]
|
|
2121
|
-
const invWeightSum = weightSum > 0.0001 ? 1.0 / weightSum : 1.0
|
|
2122
|
-
const normalizedWeights = weightSum > 0.0001 ? weights.map((w) => w * invWeightSum) : [1.0, 0.0, 0.0, 0.0]
|
|
2123
|
-
|
|
2124
|
-
// Apply skinning transformation (same as shader)
|
|
2125
|
-
let skinnedPosition = new Vec3(0, 0, 0)
|
|
2126
|
-
|
|
1992
|
+
const j0 = skinning.joints[vertexIndex * 4], j1 = skinning.joints[vertexIndex * 4 + 1], j2 = skinning.joints[vertexIndex * 4 + 2], j3 = skinning.joints[vertexIndex * 4 + 3]
|
|
1993
|
+
const w0 = skinning.weights[vertexIndex * 4] / 255, w1 = skinning.weights[vertexIndex * 4 + 1] / 255, w2 = skinning.weights[vertexIndex * 4 + 2] / 255, w3 = skinning.weights[vertexIndex * 4 + 3] / 255
|
|
1994
|
+
const ws = w0 + w1 + w2 + w3
|
|
1995
|
+
const nw = ws > 0.0001 ? [w0 / ws, w1 / ws, w2 / ws, w3 / ws] : [1, 0, 0, 0]
|
|
1996
|
+
let sp = new Vec3(0, 0, 0)
|
|
2127
1997
|
for (let j = 0; j < 4; j++) {
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
const transformed = transformByMatrix(skinMatrices, matrixOffset, position)
|
|
2132
|
-
skinnedPosition = skinnedPosition.add(transformed.scale(weight))
|
|
2133
|
-
}
|
|
1998
|
+
if (nw[j] <= 0) continue
|
|
1999
|
+
const transformed = transformByMatrix(skinMatrices, [j0, j1, j2, j3][j] * 16, position)
|
|
2000
|
+
sp = sp.add(transformed.scale(nw[j]))
|
|
2134
2001
|
}
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
vertices[i] =
|
|
2138
|
-
vertices[i +
|
|
2139
|
-
vertices[i +
|
|
2140
|
-
vertices[i +
|
|
2141
|
-
vertices[i +
|
|
2142
|
-
vertices[i +
|
|
2143
|
-
vertices[i + 6] = baseVertices[i + 6] // UV X
|
|
2144
|
-
vertices[i + 7] = baseVertices[i + 7] // UV Y
|
|
2002
|
+
vertices[i] = sp.x
|
|
2003
|
+
vertices[i + 1] = sp.y
|
|
2004
|
+
vertices[i + 2] = sp.z
|
|
2005
|
+
vertices[i + 3] = baseVertices[i + 3]
|
|
2006
|
+
vertices[i + 4] = baseVertices[i + 4]
|
|
2007
|
+
vertices[i + 5] = baseVertices[i + 5]
|
|
2008
|
+
vertices[i + 6] = baseVertices[i + 6]
|
|
2009
|
+
vertices[i + 7] = baseVertices[i + 7]
|
|
2145
2010
|
}
|
|
2146
2011
|
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
// Get triangle vertices in world space (first 3 floats are position)
|
|
2162
|
-
const v0 = new Vec3(vertices[idx0], vertices[idx0 + 1], vertices[idx0 + 2])
|
|
2163
|
-
const v1 = new Vec3(vertices[idx1], vertices[idx1 + 1], vertices[idx1 + 2])
|
|
2164
|
-
const v2 = new Vec3(vertices[idx2], vertices[idx2 + 1], vertices[idx2 + 2])
|
|
2165
|
-
|
|
2166
|
-
// Find which material this triangle belongs to
|
|
2167
|
-
// Each material has mat.vertexCount indices (3 per triangle)
|
|
2168
|
-
let triangleMaterialIndex = -1
|
|
2169
|
-
let indexOffset = 0
|
|
2170
|
-
for (let matIdx = 0; matIdx < materials.length; matIdx++) {
|
|
2171
|
-
const mat = materials[matIdx]
|
|
2172
|
-
if (i >= indexOffset && i < indexOffset + mat.vertexCount) {
|
|
2173
|
-
triangleMaterialIndex = matIdx
|
|
2174
|
-
break
|
|
2012
|
+
for (let i = 0; i < indices.length; i += 3) {
|
|
2013
|
+
const idx0 = indices[i] * 8, idx1 = indices[i + 1] * 8, idx2 = indices[i + 2] * 8
|
|
2014
|
+
const v0 = new Vec3(vertices[idx0], vertices[idx0 + 1], vertices[idx0 + 2])
|
|
2015
|
+
const v1 = new Vec3(vertices[idx1], vertices[idx1 + 1], vertices[idx1 + 2])
|
|
2016
|
+
const v2 = new Vec3(vertices[idx2], vertices[idx2 + 1], vertices[idx2 + 2])
|
|
2017
|
+
let triangleMaterialIndex = -1
|
|
2018
|
+
let indexOffset = 0
|
|
2019
|
+
for (let matIdx = 0; matIdx < materials.length; matIdx++) {
|
|
2020
|
+
if (i >= indexOffset && i < indexOffset + materials[matIdx].vertexCount) {
|
|
2021
|
+
triangleMaterialIndex = matIdx
|
|
2022
|
+
break
|
|
2023
|
+
}
|
|
2024
|
+
indexOffset += materials[matIdx].vertexCount
|
|
2175
2025
|
}
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
// Ray-triangle intersection test (Möller-Trumbore algorithm)
|
|
2186
|
-
const edge1 = v1.subtract(v0)
|
|
2187
|
-
const edge2 = v2.subtract(v0)
|
|
2188
|
-
const h = rayDirection.cross(edge2)
|
|
2189
|
-
const a = edge1.dot(h)
|
|
2190
|
-
|
|
2191
|
-
if (Math.abs(a) < 0.0001) continue // Ray is parallel to triangle
|
|
2192
|
-
|
|
2193
|
-
const f = 1.0 / a
|
|
2194
|
-
const s = rayOrigin.subtract(v0)
|
|
2195
|
-
const u = f * s.dot(h)
|
|
2196
|
-
|
|
2197
|
-
if (u < 0.0 || u > 1.0) continue
|
|
2198
|
-
|
|
2199
|
-
const q = s.cross(edge1)
|
|
2200
|
-
const v = f * rayDirection.dot(q)
|
|
2201
|
-
|
|
2202
|
-
if (v < 0.0 || u + v > 1.0) continue
|
|
2203
|
-
|
|
2204
|
-
// At this point we have a hit
|
|
2205
|
-
const t = f * edge2.dot(q)
|
|
2206
|
-
|
|
2207
|
-
if (t > 0.0001 && t < maxDistance) {
|
|
2208
|
-
// Backface culling: only consider front-facing triangles
|
|
2026
|
+
if (triangleMaterialIndex === -1) continue
|
|
2027
|
+
const edge1 = v1.subtract(v0), edge2 = v2.subtract(v0), h = rayDirection.cross(edge2), a = edge1.dot(h)
|
|
2028
|
+
if (Math.abs(a) < 0.0001) continue
|
|
2029
|
+
const f = 1 / a, s = rayOrigin.subtract(v0), u = f * s.dot(h)
|
|
2030
|
+
if (u < 0 || u > 1) continue
|
|
2031
|
+
const q = s.cross(edge1), v = f * rayDirection.dot(q)
|
|
2032
|
+
if (v < 0 || u + v > 1) continue
|
|
2033
|
+
const t = f * edge2.dot(q)
|
|
2034
|
+
if (t <= 0.0001 || t >= maxDistance) continue
|
|
2209
2035
|
const triangleNormal = edge1.cross(edge2).normalize()
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
if (!closestHit || t < closestHit.distance) {
|
|
2214
|
-
closestHit = {
|
|
2215
|
-
materialName: materials[triangleMaterialIndex].name,
|
|
2216
|
-
distance: t,
|
|
2217
|
-
}
|
|
2218
|
-
}
|
|
2036
|
+
if (triangleNormal.dot(rayDirection) >= 0) continue
|
|
2037
|
+
if (!closest || t < closest.distance) {
|
|
2038
|
+
closest = { modelName: inst.name, materialName: materials[triangleMaterialIndex].name, distance: t }
|
|
2219
2039
|
}
|
|
2220
2040
|
}
|
|
2221
|
-
}
|
|
2041
|
+
})
|
|
2222
2042
|
|
|
2223
|
-
// Call the callback with the result
|
|
2224
2043
|
if (this.onRaycast) {
|
|
2225
|
-
|
|
2044
|
+
const hit = closest as { modelName: string; materialName: string; distance: number } | null
|
|
2045
|
+
this.onRaycast(hit?.modelName ?? "", hit?.materialName ?? null, screenX, screenY)
|
|
2226
2046
|
}
|
|
2227
2047
|
}
|
|
2228
2048
|
|
|
2229
|
-
// Render strategy: 1) Opaque non-eye/hair 2) Eyes (stencil=1) 3) Hair (depth pre-pass + split by stencil) 4) Transparent
|
|
2230
2049
|
public render() {
|
|
2231
|
-
if (this.multisampleTexture
|
|
2232
|
-
const currentTime = performance.now()
|
|
2233
|
-
const deltaTime = this.lastFrameTime > 0 ? (currentTime - this.lastFrameTime) / 1000 : 0.016
|
|
2234
|
-
this.lastFrameTime = currentTime
|
|
2235
|
-
|
|
2236
|
-
this.updateCameraUniforms()
|
|
2237
|
-
this.updateRenderTarget()
|
|
2238
|
-
|
|
2239
|
-
// Update model (handles tweens, animation, physics, IK, and skin matrices)
|
|
2240
|
-
if (this.currentModel) {
|
|
2241
|
-
const verticesChanged = this.currentModel.update(deltaTime)
|
|
2242
|
-
if (verticesChanged) {
|
|
2243
|
-
this.vertexBufferNeedsUpdate = true
|
|
2244
|
-
}
|
|
2245
|
-
}
|
|
2050
|
+
if (!this.multisampleTexture || !this.camera || !this.device) return
|
|
2246
2051
|
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
this.vertexBufferNeedsUpdate = false
|
|
2251
|
-
}
|
|
2052
|
+
const currentTime = performance.now()
|
|
2053
|
+
const deltaTime = this.lastFrameTime > 0 ? (currentTime - this.lastFrameTime) / 1000 : 0.016
|
|
2054
|
+
this.lastFrameTime = currentTime
|
|
2252
2055
|
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
const encoder = this.device.createCommandEncoder()
|
|
2257
|
-
if (
|
|
2258
|
-
this.groundMode === "shadow" &&
|
|
2259
|
-
this.currentModel &&
|
|
2260
|
-
this.shadowMapDepthView &&
|
|
2261
|
-
this.shadowBindGroup
|
|
2262
|
-
) {
|
|
2263
|
-
const sp = encoder.beginRenderPass({
|
|
2264
|
-
colorAttachments: [],
|
|
2265
|
-
depthStencilAttachment: {
|
|
2266
|
-
view: this.shadowMapDepthView,
|
|
2267
|
-
depthClearValue: 1.0,
|
|
2268
|
-
depthLoadOp: "clear",
|
|
2269
|
-
depthStoreOp: "store",
|
|
2270
|
-
},
|
|
2271
|
-
})
|
|
2272
|
-
sp.setPipeline(this.shadowDepthPipeline)
|
|
2273
|
-
sp.setBindGroup(0, this.shadowBindGroup)
|
|
2274
|
-
sp.setVertexBuffer(0, this.vertexBuffer)
|
|
2275
|
-
sp.setVertexBuffer(1, this.jointsBuffer)
|
|
2276
|
-
sp.setVertexBuffer(2, this.weightsBuffer)
|
|
2277
|
-
sp.setIndexBuffer(this.indexBuffer!, "uint32")
|
|
2278
|
-
for (const draw of this.shadowDrawCalls) {
|
|
2279
|
-
if (this.shouldRenderDrawCall(draw))
|
|
2280
|
-
sp.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0)
|
|
2281
|
-
}
|
|
2282
|
-
sp.end()
|
|
2283
|
-
}
|
|
2056
|
+
this.updateCameraUniforms()
|
|
2057
|
+
this.updateRenderTarget()
|
|
2284
2058
|
|
|
2285
|
-
|
|
2059
|
+
const hasModels = this.modelInstances.size > 0
|
|
2060
|
+
if (hasModels) {
|
|
2061
|
+
this.updateInstances(deltaTime)
|
|
2062
|
+
this.updateSkinMatrices()
|
|
2063
|
+
}
|
|
2064
|
+
if (this.groundMode === "shadow") this.updateShadowLightVP()
|
|
2286
2065
|
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2066
|
+
const encoder = this.device.createCommandEncoder()
|
|
2067
|
+
if (hasModels && this.groundMode === "shadow" && this.shadowMapDepthView) {
|
|
2068
|
+
const sp = encoder.beginRenderPass({
|
|
2069
|
+
colorAttachments: [],
|
|
2070
|
+
depthStencilAttachment: {
|
|
2071
|
+
view: this.shadowMapDepthView,
|
|
2072
|
+
depthClearValue: 1.0,
|
|
2073
|
+
depthLoadOp: "clear",
|
|
2074
|
+
depthStoreOp: "store",
|
|
2075
|
+
},
|
|
2076
|
+
})
|
|
2077
|
+
sp.setPipeline(this.shadowDepthPipeline)
|
|
2078
|
+
this.forEachInstance((inst) => this.drawInstanceShadow(sp, inst))
|
|
2079
|
+
sp.end()
|
|
2080
|
+
}
|
|
2292
2081
|
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
if (draw.type === "opaque" && this.shouldRenderDrawCall(draw)) {
|
|
2297
|
-
pass.setBindGroup(0, draw.bindGroup)
|
|
2298
|
-
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0)
|
|
2299
|
-
}
|
|
2300
|
-
}
|
|
2082
|
+
const pass = encoder.beginRenderPass(this.renderPassDescriptor)
|
|
2083
|
+
if (hasModels) this.forEachInstance((inst) => this.renderOneModel(pass, inst, false))
|
|
2084
|
+
if (this.groundHasReflections) this.renderGround(pass)
|
|
2301
2085
|
|
|
2302
|
-
|
|
2303
|
-
|
|
2086
|
+
pass.end()
|
|
2087
|
+
this.device.queue.submit([encoder.finish()])
|
|
2088
|
+
this.updateStats(performance.now() - currentTime)
|
|
2089
|
+
}
|
|
2304
2090
|
|
|
2305
|
-
|
|
2091
|
+
private drawInstanceShadow(sp: GPURenderPassEncoder, inst: ModelInstance): void {
|
|
2092
|
+
sp.setBindGroup(0, inst.shadowBindGroup)
|
|
2093
|
+
sp.setVertexBuffer(0, inst.vertexBuffer)
|
|
2094
|
+
sp.setVertexBuffer(1, inst.jointsBuffer)
|
|
2095
|
+
sp.setVertexBuffer(2, inst.weightsBuffer)
|
|
2096
|
+
sp.setIndexBuffer(inst.indexBuffer, "uint32")
|
|
2097
|
+
for (const draw of inst.shadowDrawCalls) {
|
|
2098
|
+
if (this.shouldRenderDrawCall(inst, draw)) sp.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0)
|
|
2099
|
+
}
|
|
2100
|
+
}
|
|
2306
2101
|
|
|
2307
|
-
|
|
2308
|
-
|
|
2102
|
+
private renderOneModel(pass: GPURenderPassEncoder, inst: ModelInstance, useReflection: boolean, mirrorMatrix?: Mat4): void {
|
|
2103
|
+
pass.setVertexBuffer(0, inst.vertexBuffer)
|
|
2104
|
+
pass.setVertexBuffer(1, inst.jointsBuffer)
|
|
2105
|
+
pass.setVertexBuffer(2, inst.weightsBuffer)
|
|
2106
|
+
pass.setIndexBuffer(inst.indexBuffer, "uint32")
|
|
2309
2107
|
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2108
|
+
if (useReflection && mirrorMatrix) {
|
|
2109
|
+
this.writeMirrorTransformedSkinMatrices(inst, mirrorMatrix)
|
|
2110
|
+
pass.setPipeline(this.reflectionPipeline)
|
|
2111
|
+
for (const draw of inst.drawCalls) {
|
|
2112
|
+
if (draw.type === "opaque" && this.shouldRenderDrawCall(inst, draw)) {
|
|
2113
|
+
pass.setBindGroup(0, draw.bindGroup)
|
|
2114
|
+
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0)
|
|
2317
2115
|
}
|
|
2318
|
-
|
|
2319
|
-
this.drawOutlines(pass, true)
|
|
2320
2116
|
}
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2117
|
+
this.renderEyes(pass, inst, true)
|
|
2118
|
+
this.renderHair(pass, inst, true)
|
|
2119
|
+
for (const draw of inst.drawCalls) {
|
|
2120
|
+
if (draw.type === "transparent" && this.shouldRenderDrawCall(inst, draw)) {
|
|
2121
|
+
pass.setBindGroup(0, draw.bindGroup)
|
|
2122
|
+
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0)
|
|
2123
|
+
}
|
|
2324
2124
|
}
|
|
2125
|
+
this.drawOutlines(pass, inst, true, true)
|
|
2126
|
+
return
|
|
2127
|
+
}
|
|
2325
2128
|
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2129
|
+
pass.setPipeline(this.modelPipeline)
|
|
2130
|
+
for (const draw of inst.drawCalls) {
|
|
2131
|
+
if (draw.type === "opaque" && this.shouldRenderDrawCall(inst, draw)) {
|
|
2132
|
+
pass.setBindGroup(0, draw.bindGroup)
|
|
2133
|
+
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0)
|
|
2134
|
+
}
|
|
2330
2135
|
}
|
|
2136
|
+
this.renderEyes(pass, inst, false)
|
|
2137
|
+
this.drawOutlines(pass, inst, false)
|
|
2138
|
+
this.renderHair(pass, inst, false)
|
|
2139
|
+
pass.setPipeline(this.modelPipeline)
|
|
2140
|
+
for (const draw of inst.drawCalls) {
|
|
2141
|
+
if (draw.type === "transparent" && this.shouldRenderDrawCall(inst, draw)) {
|
|
2142
|
+
pass.setBindGroup(0, draw.bindGroup)
|
|
2143
|
+
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0)
|
|
2144
|
+
}
|
|
2145
|
+
}
|
|
2146
|
+
this.drawOutlines(pass, inst, true)
|
|
2331
2147
|
}
|
|
2332
2148
|
|
|
2333
2149
|
|
|
@@ -2354,31 +2170,24 @@ export class Engine {
|
|
|
2354
2170
|
}
|
|
2355
2171
|
|
|
2356
2172
|
private updateSkinMatrices() {
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
)
|
|
2367
|
-
|
|
2368
|
-
// Increment version to invalidate cached skinned vertices
|
|
2369
|
-
this.skinMatricesVersion++
|
|
2173
|
+
this.forEachInstance((inst) => {
|
|
2174
|
+
const skinMatrices = inst.model.getSkinMatrices()
|
|
2175
|
+
this.device.queue.writeBuffer(
|
|
2176
|
+
inst.skinMatrixBuffer,
|
|
2177
|
+
0,
|
|
2178
|
+
skinMatrices.buffer,
|
|
2179
|
+
skinMatrices.byteOffset,
|
|
2180
|
+
skinMatrices.byteLength
|
|
2181
|
+
)
|
|
2182
|
+
})
|
|
2370
2183
|
}
|
|
2371
2184
|
|
|
2372
|
-
private drawOutlines(pass: GPURenderPassEncoder, transparent: boolean, useReflectionPipeline
|
|
2373
|
-
if (useReflectionPipeline)
|
|
2374
|
-
// Skip outlines for reflections - not critical for the effect
|
|
2375
|
-
return
|
|
2376
|
-
}
|
|
2377
|
-
|
|
2185
|
+
private drawOutlines(pass: GPURenderPassEncoder, inst: ModelInstance, transparent: boolean, useReflectionPipeline = false) {
|
|
2186
|
+
if (useReflectionPipeline) return
|
|
2378
2187
|
pass.setPipeline(this.outlinePipeline)
|
|
2379
2188
|
const outlineType: DrawCallType = transparent ? "transparent-outline" : "opaque-outline"
|
|
2380
|
-
for (const draw of
|
|
2381
|
-
if (draw.type === outlineType && this.shouldRenderDrawCall(draw)) {
|
|
2189
|
+
for (const draw of inst.drawCalls) {
|
|
2190
|
+
if (draw.type === outlineType && this.shouldRenderDrawCall(inst, draw)) {
|
|
2382
2191
|
pass.setBindGroup(0, draw.bindGroup)
|
|
2383
2192
|
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0)
|
|
2384
2193
|
}
|
|
@@ -2436,24 +2245,16 @@ export class Engine {
|
|
|
2436
2245
|
)
|
|
2437
2246
|
}
|
|
2438
2247
|
|
|
2439
|
-
private writeMirrorTransformedSkinMatrices(mirrorMatrix: Mat4) {
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
const originalMatrices = this.currentModel.getSkinMatrices()
|
|
2248
|
+
private writeMirrorTransformedSkinMatrices(inst: ModelInstance, mirrorMatrix: Mat4) {
|
|
2249
|
+
const originalMatrices = inst.model.getSkinMatrices()
|
|
2443
2250
|
const transformedMatrices = new Float32Array(originalMatrices.length)
|
|
2444
|
-
|
|
2445
2251
|
for (let i = 0; i < originalMatrices.length; i += 16) {
|
|
2446
2252
|
const boneMatrixValues = new Float32Array(16)
|
|
2447
|
-
for (let j = 0; j < 16; j++)
|
|
2448
|
-
boneMatrixValues[j] = originalMatrices[i + j]
|
|
2449
|
-
}
|
|
2253
|
+
for (let j = 0; j < 16; j++) boneMatrixValues[j] = originalMatrices[i + j]
|
|
2450
2254
|
const boneMatrix = new Mat4(boneMatrixValues)
|
|
2451
2255
|
const transformed = mirrorMatrix.multiply(boneMatrix)
|
|
2452
|
-
for (let j = 0; j < 16; j++)
|
|
2453
|
-
transformedMatrices[i + j] = transformed.values[j]
|
|
2454
|
-
}
|
|
2256
|
+
for (let j = 0; j < 16; j++) transformedMatrices[i + j] = transformed.values[j]
|
|
2455
2257
|
}
|
|
2456
|
-
|
|
2457
|
-
this.device.queue.writeBuffer(this.skinMatrixBuffer, 0, transformedMatrices)
|
|
2258
|
+
this.device.queue.writeBuffer(inst.skinMatrixBuffer, 0, transformedMatrices)
|
|
2458
2259
|
}
|
|
2459
2260
|
}
|