reze-engine 0.36.0 → 0.37.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -3
- package/dist/camera.d.ts +17 -0
- package/dist/camera.d.ts.map +1 -1
- package/dist/camera.js +25 -0
- package/dist/engine.d.ts +94 -1
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +250 -14
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/model.d.ts +81 -0
- package/dist/model.d.ts.map +1 -1
- package/dist/model.js +192 -0
- package/dist/physics/autofit.d.ts +147 -0
- package/dist/physics/autofit.d.ts.map +1 -0
- package/dist/physics/autofit.js +501 -0
- package/dist/physics/contact.d.ts +12 -3
- package/dist/physics/contact.d.ts.map +1 -1
- package/dist/physics/contact.js +5 -2
- package/dist/physics/manifold.d.ts +32 -11
- package/dist/physics/manifold.d.ts.map +1 -1
- package/dist/physics/manifold.js +137 -57
- package/dist/physics/solver.d.ts +7 -1
- package/dist/physics/solver.d.ts.map +1 -1
- package/dist/physics/solver.js +536 -182
- package/dist/physics/world.d.ts +2 -0
- package/dist/physics/world.d.ts.map +1 -1
- package/dist/physics/world.js +16 -43
- package/dist/pmx-loader.d.ts.map +1 -1
- package/dist/pmx-loader.js +84 -44
- package/package.json +1 -1
- package/src/camera.ts +26 -0
- package/src/engine.ts +305 -18
- package/src/index.ts +11 -1
- package/src/model.ts +231 -1
- package/src/physics/contact.ts +17 -7
- package/src/physics/manifold.ts +151 -0
- package/src/physics/solver.ts +520 -184
- package/src/physics/world.ts +17 -43
- package/src/pmx-loader.ts +84 -44
package/src/engine.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Camera } from "./camera"
|
|
2
2
|
import { Mat4, Quat, Vec3 } from "./math"
|
|
3
|
-
import { Model, type Material } from "./model"
|
|
3
|
+
import { Model, MATERIAL_MORPH_MULTIPLY, type Material } from "./model"
|
|
4
4
|
import { MORPH_COMPUTE_WGSL } from "./shaders/passes/morph"
|
|
5
5
|
import { decodeTga } from "./tga-loader"
|
|
6
6
|
import { VMDLoader } from "./vmd-loader"
|
|
@@ -429,7 +429,21 @@ interface ModelInstance {
|
|
|
429
429
|
mainPerInstanceBindGroup: GPUBindGroup
|
|
430
430
|
pickPerInstanceBindGroup: GPUBindGroup
|
|
431
431
|
pickDrawCalls: PickDrawCall[]
|
|
432
|
+
/** Environment geometry added via addStage — no physics, no IK, and it
|
|
433
|
+
* suppresses the built-in ground. See addStage for why each of those. */
|
|
434
|
+
isStage: boolean
|
|
435
|
+
/** A pose pass ran since the last skin-matrix upload. Always true for cast
|
|
436
|
+
* members; false for an idle stage, which is the point. */
|
|
437
|
+
skinMatricesDirty: boolean
|
|
432
438
|
hiddenMaterials: Set<string>
|
|
439
|
+
/** Materials a material morph has driven to zero alpha. Kept apart from
|
|
440
|
+
* hiddenMaterials so a morph switching a part off never clobbers the user's
|
|
441
|
+
* own visibility toggle, and vice versa. */
|
|
442
|
+
morphHiddenMaterials: Set<string>
|
|
443
|
+
/** Material-morph targets, or null when the model has no type-8 morphs. */
|
|
444
|
+
materialMorphTargets: MaterialMorphTarget[] | null
|
|
445
|
+
/** The same targets by PMX material index, so a named offset is one lookup. */
|
|
446
|
+
materialMorphByIndex: Map<number, MaterialMorphTarget> | null
|
|
433
447
|
physics: RezePhysics | null
|
|
434
448
|
vertexBufferNeedsUpdate: boolean
|
|
435
449
|
gpuMorph: GpuMorph | null
|
|
@@ -442,6 +456,30 @@ interface ModelInstance {
|
|
|
442
456
|
styleGroupGen: Map<string, number>
|
|
443
457
|
}
|
|
444
458
|
|
|
459
|
+
/**
|
|
460
|
+
* One material a type-8 morph can reach, with the uniform block as it loaded.
|
|
461
|
+
*
|
|
462
|
+
* Material morphs are re-derived from base every time a weight changes rather
|
|
463
|
+
* than accumulated, because weights go down as well as up and a running total
|
|
464
|
+
* drifts. The buffer is already COPY_DST, so this is a writeBuffer, not a
|
|
465
|
+
* rebuild.
|
|
466
|
+
*/
|
|
467
|
+
interface MaterialMorphTarget {
|
|
468
|
+
/** Index into the PMX material array — what MaterialMorphOffset points at. */
|
|
469
|
+
pmxIndex: number
|
|
470
|
+
materialName: string
|
|
471
|
+
buffer: GPUBuffer
|
|
472
|
+
/** The 16-float MaterialUniforms block as createMaterialUniformBuffer wrote it. */
|
|
473
|
+
base: Float32Array
|
|
474
|
+
/** Scratch for the morphed block, so the per-change pass allocates nothing. */
|
|
475
|
+
work: Float32Array
|
|
476
|
+
/** What was last uploaded. `applyMorphs` marks weights dirty on every frame of
|
|
477
|
+
* any clip carrying morph tracks — i.e. every character with a face VMD — so
|
|
478
|
+
* without this the pass would re-upload byte-identical material blocks
|
|
479
|
+
* forever on behalf of a switch that never moves. */
|
|
480
|
+
last: Float32Array
|
|
481
|
+
}
|
|
482
|
+
|
|
445
483
|
// Per-model GPU vertex-morph compute state. Present only for models with vertex morphs.
|
|
446
484
|
interface GpuMorph {
|
|
447
485
|
bindGroup: GPUBindGroup
|
|
@@ -3083,7 +3121,44 @@ export class Engine {
|
|
|
3083
3121
|
return model
|
|
3084
3122
|
}
|
|
3085
3123
|
|
|
3086
|
-
|
|
3124
|
+
/** loadModel's folder/zip path for a stage. Shares the whole prelude — only
|
|
3125
|
+
* what the PMX becomes differs. */
|
|
3126
|
+
async loadStage(
|
|
3127
|
+
name: string,
|
|
3128
|
+
options: LoadModelFromFilesOptions & { transform?: Partial<ModelTransform> },
|
|
3129
|
+
): Promise<Model> {
|
|
3130
|
+
const { model, pmxKey, reader } = await this.openPmxFromFiles(name, options)
|
|
3131
|
+
await this.addStage(model, pmxKey, { name, transform: options.transform, assetReader: reader })
|
|
3132
|
+
return model
|
|
3133
|
+
}
|
|
3134
|
+
|
|
3135
|
+
/** Read a PMX out of a picked folder / expanded zip. Shared by loadModel and
|
|
3136
|
+
* loadStage so the file-map and path handling exist in exactly one place. */
|
|
3137
|
+
private async openPmxFromFiles(
|
|
3138
|
+
name: string,
|
|
3139
|
+
options: LoadModelFromFilesOptions,
|
|
3140
|
+
): Promise<{ model: Model; pmxKey: string; reader: AssetReader }> {
|
|
3141
|
+
const pmxFile = options.pmxFile ?? findFirstPmxFileInList(options.files)
|
|
3142
|
+
if (!pmxFile) throw new Error("No .pmx file found in the selected folder")
|
|
3143
|
+
const map = fileListToMap(options.files)
|
|
3144
|
+
// `||`, not `??`: flat-picked files carry webkitRelativePath === "" (see
|
|
3145
|
+
// fileListToMap) — `""` must fall through to the filename.
|
|
3146
|
+
const pmxKey = normalizeAssetPath(
|
|
3147
|
+
(pmxFile as File & { webkitRelativePath?: string }).webkitRelativePath || pmxFile.name,
|
|
3148
|
+
)
|
|
3149
|
+
const reader = createFileMapAssetReader(map)
|
|
3150
|
+
const model = await PmxLoader.loadFromReader(reader, pmxKey)
|
|
3151
|
+
model.setName(name)
|
|
3152
|
+
return { model, pmxKey, reader }
|
|
3153
|
+
}
|
|
3154
|
+
|
|
3155
|
+
async addModel(
|
|
3156
|
+
model: Model,
|
|
3157
|
+
pmxPath: string,
|
|
3158
|
+
name?: string,
|
|
3159
|
+
assetReader?: AssetReader,
|
|
3160
|
+
options?: { stage?: boolean },
|
|
3161
|
+
): Promise<string> {
|
|
3087
3162
|
const requested = name ?? model.name
|
|
3088
3163
|
let key = requested
|
|
3089
3164
|
let n = 1
|
|
@@ -3093,10 +3168,46 @@ export class Engine {
|
|
|
3093
3168
|
const reader = assetReader ?? createFetchAssetReader()
|
|
3094
3169
|
const basePath = deriveBasePathFromPmxPath(pmxPath)
|
|
3095
3170
|
model.setAssetContext(reader, basePath)
|
|
3096
|
-
await this.setupModelInstance(key, model, basePath, reader)
|
|
3171
|
+
await this.setupModelInstance(key, model, basePath, reader, options?.stage ?? false)
|
|
3097
3172
|
return key
|
|
3098
3173
|
}
|
|
3099
3174
|
|
|
3175
|
+
/**
|
|
3176
|
+
* Add a PMX as the scene's environment rather than as a character.
|
|
3177
|
+
*
|
|
3178
|
+
* A stage is the same geometry and the same materials — style groups and
|
|
3179
|
+
* shader graphs work on it unchanged, which is the whole reason pure-PMX
|
|
3180
|
+
* stages are worth supporting — but it is not a performer:
|
|
3181
|
+
*
|
|
3182
|
+
* - no physics. A stage's rigidbodies are set dressing for MMD's solver and
|
|
3183
|
+
* cost a full simulation island for scenery that never moves.
|
|
3184
|
+
* - no IK. Nothing drives a stage's chains, and solving them every frame is
|
|
3185
|
+
* pure waste on what is usually the heaviest mesh in the scene.
|
|
3186
|
+
* - no per-frame pose work while it is idle: with no clip and no morph
|
|
3187
|
+
* change there is nothing to recompute, so update is skipped entirely.
|
|
3188
|
+
* - it owns the floor. See groundIsSuppressed — the built-in ground plane
|
|
3189
|
+
* and a stage's own floor both sit at y=0 and z-fight.
|
|
3190
|
+
*
|
|
3191
|
+
* Bone and material morphs still apply, because that is how a stage's doors,
|
|
3192
|
+
* lifts and colour switches are rigged.
|
|
3193
|
+
*/
|
|
3194
|
+
async addStage(
|
|
3195
|
+
model: Model,
|
|
3196
|
+
pmxPath: string,
|
|
3197
|
+
options?: { name?: string; transform?: Partial<ModelTransform>; assetReader?: AssetReader },
|
|
3198
|
+
): Promise<string> {
|
|
3199
|
+
const key = await this.addModel(model, pmxPath, options?.name, options?.assetReader, { stage: true })
|
|
3200
|
+
if (options?.transform) this.setModelTransform(key, options.transform)
|
|
3201
|
+
return key
|
|
3202
|
+
}
|
|
3203
|
+
|
|
3204
|
+
/** True while a stage is in the scene, which is when the built-in ground plane
|
|
3205
|
+
* must not draw. */
|
|
3206
|
+
groundIsSuppressed(): boolean {
|
|
3207
|
+
for (const inst of this.modelInstances.values()) if (inst.isStage) return true
|
|
3208
|
+
return false
|
|
3209
|
+
}
|
|
3210
|
+
|
|
3100
3211
|
removeModel(name: string): void {
|
|
3101
3212
|
const inst = this.modelInstances.get(name)
|
|
3102
3213
|
if (!inst) return
|
|
@@ -3144,12 +3255,18 @@ export class Engine {
|
|
|
3144
3255
|
* character — its colliders won't scale; scale stages (which are typically physics-free).
|
|
3145
3256
|
*/
|
|
3146
3257
|
setModelTransform(name: string, transform: Partial<ModelTransform>): void {
|
|
3147
|
-
const
|
|
3148
|
-
|
|
3258
|
+
const inst = this.modelInstances.get(name)
|
|
3259
|
+
const model = inst?.model
|
|
3260
|
+
if (!inst || !model) return
|
|
3149
3261
|
if (transform.position) model.setPosition(transform.position)
|
|
3150
3262
|
if (transform.rotation) model.setRotation(transform.rotation)
|
|
3151
3263
|
if (transform.scale !== undefined) model.setScale(transform.scale)
|
|
3152
3264
|
if (transform.visible !== undefined) model.setVisible(transform.visible)
|
|
3265
|
+
// The root transform is baked into the skin matrices, so moving a model is a
|
|
3266
|
+
// reason to re-upload them even though no pose pass ran. A cast member gets
|
|
3267
|
+
// one every frame anyway; an idle stage would otherwise never see the change
|
|
3268
|
+
// — which is exactly the case this API exists to serve.
|
|
3269
|
+
inst.skinMatricesDirty = true
|
|
3153
3270
|
}
|
|
3154
3271
|
|
|
3155
3272
|
/** Read a model's scene transform (for serialization into a scene descriptor). */
|
|
@@ -3316,8 +3433,22 @@ export class Engine {
|
|
|
3316
3433
|
let physicsMs = 0
|
|
3317
3434
|
this.forEachInstance((inst) => {
|
|
3318
3435
|
const tAnim = performance.now()
|
|
3319
|
-
|
|
3436
|
+
// A stage never solves IK — nothing drives its chains — and skips the pose
|
|
3437
|
+
// pass entirely while it is idle. Morph changes still come through, since
|
|
3438
|
+
// that is the one thing a stage's controls do move.
|
|
3439
|
+
const stageIdle = inst.isStage && inst.model.isIdle()
|
|
3440
|
+
let verticesChanged = false
|
|
3441
|
+
if (!stageIdle) {
|
|
3442
|
+
verticesChanged = inst.model.update(deltaTime, inst.isStage ? false : this.ikEnabled)
|
|
3443
|
+
inst.skinMatricesDirty = true
|
|
3444
|
+
}
|
|
3320
3445
|
animMs += performance.now() - tAnim
|
|
3446
|
+
// Material morphs ride the same weight change as vertex morphs but land in
|
|
3447
|
+
// uniform buffers, so they consume their own flag — a model whose only
|
|
3448
|
+
// morphs are material morphs never enters the GPU vertex path below.
|
|
3449
|
+
if (inst.materialMorphTargets && inst.model.consumeAuxMorphDirty()) {
|
|
3450
|
+
this.applyMaterialMorphs(inst)
|
|
3451
|
+
}
|
|
3321
3452
|
if (inst.gpuMorph) {
|
|
3322
3453
|
// GPU path: on a weight change, upload effective weights (thresholding tiny values
|
|
3323
3454
|
// to 0 to match the CPU skip) and flag the compute dispatch for this frame.
|
|
@@ -3401,6 +3532,7 @@ export class Engine {
|
|
|
3401
3532
|
model: Model,
|
|
3402
3533
|
basePath: string,
|
|
3403
3534
|
assetReader: AssetReader,
|
|
3535
|
+
isStage = false,
|
|
3404
3536
|
): Promise<void> {
|
|
3405
3537
|
const vertices = model.getVertices()
|
|
3406
3538
|
const skinning = model.getSkinning()
|
|
@@ -3458,7 +3590,10 @@ export class Engine {
|
|
|
3458
3590
|
this.device.queue.writeBuffer(indexBuffer, 0, indices)
|
|
3459
3591
|
|
|
3460
3592
|
const rbs = model.getRigidbodies()
|
|
3461
|
-
|
|
3593
|
+
// A stage never simulates, so its bodies are never built — constructing the
|
|
3594
|
+
// solver for the heaviest mesh in the scene and dropping it afterwards was
|
|
3595
|
+
// both wasted work and an invariant maintained in the wrong place.
|
|
3596
|
+
const physics = !isStage && rbs.length > 0 ? new RezePhysics(rbs, model.getJoints()) : null
|
|
3462
3597
|
// Adopt the scene's air, or a model added mid-session would fall under
|
|
3463
3598
|
// different gravity from the ones already on stage.
|
|
3464
3599
|
if (physics) {
|
|
@@ -3510,7 +3645,13 @@ export class Engine {
|
|
|
3510
3645
|
mainPerInstanceBindGroup,
|
|
3511
3646
|
pickPerInstanceBindGroup,
|
|
3512
3647
|
pickDrawCalls: [],
|
|
3648
|
+
isStage,
|
|
3649
|
+
// Seeded true: the bind pose has to reach the GPU once before any frame.
|
|
3650
|
+
skinMatricesDirty: true,
|
|
3513
3651
|
hiddenMaterials: new Set(),
|
|
3652
|
+
morphHiddenMaterials: new Set(),
|
|
3653
|
+
materialMorphTargets: null,
|
|
3654
|
+
materialMorphByIndex: null,
|
|
3514
3655
|
physics,
|
|
3515
3656
|
vertexBufferNeedsUpdate: false,
|
|
3516
3657
|
gpuMorph,
|
|
@@ -3803,9 +3944,25 @@ export class Engine {
|
|
|
3803
3944
|
// 頭 bone index for the eye shader's rear-view gate (-1 when absent).
|
|
3804
3945
|
const headBoneIndex = model.getSkeleton().bones.findIndex((b) => b.name === "頭")
|
|
3805
3946
|
|
|
3947
|
+
// Materials a type-8 morph can reach. -1 in an offset means "all of them",
|
|
3948
|
+
// so the presence of ANY material morph makes every material a target.
|
|
3949
|
+
const morphedMaterials = new Set<number>()
|
|
3950
|
+
for (const morph of model.getMorphing().morphs) {
|
|
3951
|
+
if (morph.type !== 8 || !morph.materialOffsets) continue
|
|
3952
|
+
for (const off of morph.materialOffsets) {
|
|
3953
|
+
if (off.materialIndex < 0) for (let i = 0; i < materials.length; i++) morphedMaterials.add(i)
|
|
3954
|
+
else morphedMaterials.add(off.materialIndex)
|
|
3955
|
+
}
|
|
3956
|
+
}
|
|
3957
|
+
const morphTargets: MaterialMorphTarget[] = []
|
|
3958
|
+
|
|
3806
3959
|
let currentIndexOffset = 0
|
|
3807
3960
|
let materialId = 0
|
|
3961
|
+
// The PMX index, which is what a material morph points at — distinct from
|
|
3962
|
+
// materialId, which only counts materials that produced a draw.
|
|
3963
|
+
let pmxMaterialIndex = -1
|
|
3808
3964
|
for (const mat of materials) {
|
|
3965
|
+
pmxMaterialIndex++
|
|
3809
3966
|
const indexCount = mat.vertexCount
|
|
3810
3967
|
if (indexCount === 0) continue
|
|
3811
3968
|
materialId++
|
|
@@ -3857,6 +4014,19 @@ export class Engine {
|
|
|
3857
4014
|
|
|
3858
4015
|
const materialUniformBuffer = this.createMaterialUniformBuffer(prefix + mat.name, mat, sphereMode, headBoneIndex)
|
|
3859
4016
|
inst.gpuBuffers.push(materialUniformBuffer)
|
|
4017
|
+
if (morphedMaterials.has(pmxMaterialIndex)) {
|
|
4018
|
+
const base = this.materialUniformData(mat, sphereMode, headBoneIndex)
|
|
4019
|
+
morphTargets.push({
|
|
4020
|
+
pmxIndex: pmxMaterialIndex,
|
|
4021
|
+
materialName: mat.name,
|
|
4022
|
+
buffer: materialUniformBuffer,
|
|
4023
|
+
base,
|
|
4024
|
+
work: new Float32Array(base.length),
|
|
4025
|
+
// Seeded from base: that is what createMaterialUniformBuffer already
|
|
4026
|
+
// uploaded, so an unmorphed material never writes a first time.
|
|
4027
|
+
last: Float32Array.from(base),
|
|
4028
|
+
})
|
|
4029
|
+
}
|
|
3860
4030
|
|
|
3861
4031
|
const textureView = diffuseTexture.createView()
|
|
3862
4032
|
const baseBindGroupEntries: GPUBindGroupEntry[] = [
|
|
@@ -3878,8 +4048,13 @@ export class Engine {
|
|
|
3878
4048
|
// its own hull where it is see-through instead of us skipping it here.
|
|
3879
4049
|
// Drawn interleaved right after this material's color draw (babylon-mmd's
|
|
3880
4050
|
// per-mesh afterRender outline stage) — see drawMaterials.
|
|
4051
|
+
// Stages get no outline hulls. The inverted hull is a SECOND full draw of
|
|
4052
|
+
// the material's geometry, and stage PMX routinely set the edge flag across
|
|
4053
|
+
// every material — on the heaviest mesh in the scene that doubles the
|
|
4054
|
+
// geometry submitted per frame to draw cartoon outlines around
|
|
4055
|
+
// architecture, which is not the look anyone is after.
|
|
3881
4056
|
let outline: DrawCall["outline"]
|
|
3882
|
-
if ((mat.edgeFlag & 0x10) !== 0 && mat.edgeSize > 0) {
|
|
4057
|
+
if (!inst.isStage && (mat.edgeFlag & 0x10) !== 0 && mat.edgeSize > 0) {
|
|
3883
4058
|
const materialUniformData = new Float32Array([
|
|
3884
4059
|
mat.edgeColor[0],
|
|
3885
4060
|
mat.edgeColor[1],
|
|
@@ -3939,16 +4114,18 @@ export class Engine {
|
|
|
3939
4114
|
// by render-class when groups are assigned. Array.sort is stable → PMX order preserved
|
|
3940
4115
|
// within a bucket.
|
|
3941
4116
|
this.sortDrawCalls(inst)
|
|
4117
|
+
|
|
4118
|
+
inst.materialMorphTargets = morphTargets.length > 0 ? morphTargets : null
|
|
4119
|
+
inst.materialMorphByIndex = inst.materialMorphTargets
|
|
4120
|
+
? new Map(morphTargets.map((t) => [t.pmxIndex, t]))
|
|
4121
|
+
: null
|
|
4122
|
+
// Seed from the current weights: a scene can open with a switch already on.
|
|
4123
|
+
if (inst.materialMorphTargets) this.applyMaterialMorphs(inst)
|
|
3942
4124
|
}
|
|
3943
4125
|
|
|
3944
|
-
|
|
3945
|
-
|
|
3946
|
-
|
|
3947
|
-
sphereMode: number,
|
|
3948
|
-
headBoneIndex: number,
|
|
3949
|
-
): GPUBuffer {
|
|
3950
|
-
// Matches the WGSL MaterialUniforms struct in common.ts — 64 bytes
|
|
3951
|
-
// (diffuse+alpha | ambient+shininess | specular+sphereMode | headIdx+pad).
|
|
4126
|
+
/** Matches the WGSL MaterialUniforms struct in common.ts — 64 bytes
|
|
4127
|
+
* (diffuse+alpha | ambient+shininess | specular+sphereMode | headIdx+pad). */
|
|
4128
|
+
private materialUniformData(mat: Material, sphereMode: number, headBoneIndex: number): Float32Array {
|
|
3952
4129
|
const data = new Float32Array(16)
|
|
3953
4130
|
data[0] = mat.diffuse[0]
|
|
3954
4131
|
data[1] = mat.diffuse[1]
|
|
@@ -3963,7 +4140,107 @@ export class Engine {
|
|
|
3963
4140
|
data[10] = mat.specular[2]
|
|
3964
4141
|
data[11] = sphereMode
|
|
3965
4142
|
data[12] = headBoneIndex
|
|
3966
|
-
return
|
|
4143
|
+
return data
|
|
4144
|
+
}
|
|
4145
|
+
|
|
4146
|
+
private createMaterialUniformBuffer(
|
|
4147
|
+
label: string,
|
|
4148
|
+
mat: Material,
|
|
4149
|
+
sphereMode: number,
|
|
4150
|
+
headBoneIndex: number,
|
|
4151
|
+
): GPUBuffer {
|
|
4152
|
+
return this.createUniformBuffer(
|
|
4153
|
+
`material uniform: ${label}`,
|
|
4154
|
+
this.materialUniformData(mat, sphereMode, headBoneIndex),
|
|
4155
|
+
)
|
|
4156
|
+
}
|
|
4157
|
+
|
|
4158
|
+
/**
|
|
4159
|
+
* Re-derive every morph-targeted material's uniform block from base and push
|
|
4160
|
+
* the ones that moved.
|
|
4161
|
+
*
|
|
4162
|
+
* Blend maths follow MMD (and babylon-mmd's _applyMaterialMorph): multiply
|
|
4163
|
+
* lerps from base toward base*morph, add offsets from base. Weight 0 must
|
|
4164
|
+
* therefore land exactly on base, which is why this recomputes rather than
|
|
4165
|
+
* accumulates.
|
|
4166
|
+
*
|
|
4167
|
+
* A material driven to zero alpha is dropped from the draw instead of being
|
|
4168
|
+
* written through: the opaque/transparent bucket is decided at load from the
|
|
4169
|
+
* PMX alpha, so an opaque draw cannot become see-through by uniform alone.
|
|
4170
|
+
* Full-off is the switch stage artists actually ship (帽子消失 and friends);
|
|
4171
|
+
* a partial fade on a material that loaded opaque still will not blend.
|
|
4172
|
+
*/
|
|
4173
|
+
private applyMaterialMorphs(inst: ModelInstance): void {
|
|
4174
|
+
const targets = inst.materialMorphTargets
|
|
4175
|
+
if (!targets) return
|
|
4176
|
+
const morphs = inst.model.getMorphing().morphs
|
|
4177
|
+
const weights = inst.model.getEffectiveMorphWeights()
|
|
4178
|
+
|
|
4179
|
+
for (const target of targets) {
|
|
4180
|
+
target.work.set(target.base)
|
|
4181
|
+
}
|
|
4182
|
+
|
|
4183
|
+
for (let i = 0; i < morphs.length; i++) {
|
|
4184
|
+
const w = weights[i]
|
|
4185
|
+
if (w < 0.0001) continue
|
|
4186
|
+
const morph = morphs[i]
|
|
4187
|
+
if (morph.type !== 8 || !morph.materialOffsets) continue
|
|
4188
|
+
for (const off of morph.materialOffsets) {
|
|
4189
|
+
// A named material resolves in one lookup. Only the -1 wildcard walks
|
|
4190
|
+
// every target — and once any offset uses it, every material in the
|
|
4191
|
+
// model is a target, so scanning per offset would be quadratic on the
|
|
4192
|
+
// large stages this is meant to serve.
|
|
4193
|
+
const hit = off.materialIndex >= 0 ? inst.materialMorphByIndex?.get(off.materialIndex) : undefined
|
|
4194
|
+
const affected = off.materialIndex >= 0 ? (hit ? [hit] : []) : targets
|
|
4195
|
+
for (const target of affected) {
|
|
4196
|
+
const d = target.work
|
|
4197
|
+
if (off.offsetType === MATERIAL_MORPH_MULTIPLY) {
|
|
4198
|
+
d[0] += (d[0] * off.diffuse[0] - d[0]) * w
|
|
4199
|
+
d[1] += (d[1] * off.diffuse[1] - d[1]) * w
|
|
4200
|
+
d[2] += (d[2] * off.diffuse[2] - d[2]) * w
|
|
4201
|
+
d[3] += (d[3] * off.diffuse[3] - d[3]) * w
|
|
4202
|
+
d[4] += (d[4] * off.ambient[0] - d[4]) * w
|
|
4203
|
+
d[5] += (d[5] * off.ambient[1] - d[5]) * w
|
|
4204
|
+
d[6] += (d[6] * off.ambient[2] - d[6]) * w
|
|
4205
|
+
d[7] += (d[7] * off.shininess - d[7]) * w
|
|
4206
|
+
d[8] += (d[8] * off.specular[0] - d[8]) * w
|
|
4207
|
+
d[9] += (d[9] * off.specular[1] - d[9]) * w
|
|
4208
|
+
d[10] += (d[10] * off.specular[2] - d[10]) * w
|
|
4209
|
+
} else {
|
|
4210
|
+
d[0] += off.diffuse[0] * w
|
|
4211
|
+
d[1] += off.diffuse[1] * w
|
|
4212
|
+
d[2] += off.diffuse[2] * w
|
|
4213
|
+
d[3] += off.diffuse[3] * w
|
|
4214
|
+
d[4] += off.ambient[0] * w
|
|
4215
|
+
d[5] += off.ambient[1] * w
|
|
4216
|
+
d[6] += off.ambient[2] * w
|
|
4217
|
+
d[7] += off.shininess * w
|
|
4218
|
+
d[8] += off.specular[0] * w
|
|
4219
|
+
d[9] += off.specular[1] * w
|
|
4220
|
+
d[10] += off.specular[2] * w
|
|
4221
|
+
}
|
|
4222
|
+
}
|
|
4223
|
+
}
|
|
4224
|
+
}
|
|
4225
|
+
|
|
4226
|
+
inst.morphHiddenMaterials.clear()
|
|
4227
|
+
for (const target of targets) {
|
|
4228
|
+
const d = target.work
|
|
4229
|
+
// Alpha is the switch; clamp the rest so a stacked multiply cannot send a
|
|
4230
|
+
// colour negative and light the material from the inside.
|
|
4231
|
+
for (let k = 0; k < 11; k++) if (d[k] < 0) d[k] = 0
|
|
4232
|
+
if (d[3] < 0.0001) inst.morphHiddenMaterials.add(target.materialName)
|
|
4233
|
+
let changed = false
|
|
4234
|
+
for (let k = 0; k < 11; k++) {
|
|
4235
|
+
if (d[k] !== target.last[k]) {
|
|
4236
|
+
changed = true
|
|
4237
|
+
break
|
|
4238
|
+
}
|
|
4239
|
+
}
|
|
4240
|
+
if (!changed) continue
|
|
4241
|
+
target.last.set(d)
|
|
4242
|
+
this.device.queue.writeBuffer(target.buffer, 0, d as ArrayBufferView<ArrayBuffer>)
|
|
4243
|
+
}
|
|
3967
4244
|
}
|
|
3968
4245
|
|
|
3969
4246
|
private createUniformBuffer(label: string, data: Float32Array | Uint32Array): GPUBuffer {
|
|
@@ -3977,7 +4254,7 @@ export class Engine {
|
|
|
3977
4254
|
}
|
|
3978
4255
|
|
|
3979
4256
|
private shouldRenderDrawCall(inst: ModelInstance, drawCall: DrawCall): boolean {
|
|
3980
|
-
return !inst.hiddenMaterials.has(drawCall.materialName)
|
|
4257
|
+
return !inst.hiddenMaterials.has(drawCall.materialName) && !inst.morphHiddenMaterials.has(drawCall.materialName)
|
|
3981
4258
|
}
|
|
3982
4259
|
|
|
3983
4260
|
private async createTextureFromLogicalPath(inst: ModelInstance, logicalPath: string): Promise<GPUTexture | null> {
|
|
@@ -4115,6 +4392,11 @@ export class Engine {
|
|
|
4115
4392
|
}
|
|
4116
4393
|
|
|
4117
4394
|
private renderGround(pass: GPURenderPassEncoder) {
|
|
4395
|
+
// A stage brings its own floor. Both sit at y=0, so drawing the built-in
|
|
4396
|
+
// plane underneath produces z-fighting across the whole scene — enforced
|
|
4397
|
+
// here rather than left to callers, who cannot see the conflict coming.
|
|
4398
|
+
// hasGround is left alone: remove the stage and the ground comes back.
|
|
4399
|
+
if (this.groundIsSuppressed()) return
|
|
4118
4400
|
if (!this.hasGround || !this.groundVertexBuffer || !this.groundIndexBuffer || !this.groundDrawCall) return
|
|
4119
4401
|
pass.setPipeline(this.groundShadowPipeline)
|
|
4120
4402
|
pass.setVertexBuffer(0, this.groundVertexBuffer)
|
|
@@ -5462,6 +5744,10 @@ export class Engine {
|
|
|
5462
5744
|
|
|
5463
5745
|
private updateSkinMatrices() {
|
|
5464
5746
|
this.forEachInstance((inst) => {
|
|
5747
|
+
// Only a pose pass can change these, and an idle stage did not run one —
|
|
5748
|
+
// re-uploading bones×64 bytes for scenery that never moves is the one
|
|
5749
|
+
// per-frame cost a stage would otherwise still pay in full.
|
|
5750
|
+
if (!inst.skinMatricesDirty) return
|
|
5465
5751
|
const skinMatrices = inst.model.getSkinMatrices()
|
|
5466
5752
|
this.device.queue.writeBuffer(
|
|
5467
5753
|
inst.skinMatrixBuffer,
|
|
@@ -5470,6 +5756,7 @@ export class Engine {
|
|
|
5470
5756
|
skinMatrices.byteOffset,
|
|
5471
5757
|
skinMatrices.byteLength,
|
|
5472
5758
|
)
|
|
5759
|
+
inst.skinMatricesDirty = false
|
|
5473
5760
|
})
|
|
5474
5761
|
}
|
|
5475
5762
|
|
package/src/index.ts
CHANGED
|
@@ -52,7 +52,17 @@ export { BODY_GRAPH } from "./graph/presets/body"
|
|
|
52
52
|
export { STOCKINGS_GRAPH } from "./graph/presets/stockings"
|
|
53
53
|
export { EYE_GRAPH } from "./graph/presets/eye"
|
|
54
54
|
export { FACE_GRAPH } from "./graph/presets/face"
|
|
55
|
-
export {
|
|
55
|
+
export {
|
|
56
|
+
Model,
|
|
57
|
+
MATERIAL_MORPH_MULTIPLY,
|
|
58
|
+
MATERIAL_MORPH_ADD,
|
|
59
|
+
type ClipEventInfo,
|
|
60
|
+
type Morph,
|
|
61
|
+
type Morphing,
|
|
62
|
+
type BoneMorphOffset,
|
|
63
|
+
type MaterialMorphOffset,
|
|
64
|
+
type UvMorphOffset,
|
|
65
|
+
} from "./model"
|
|
56
66
|
export { Vec3, Quat, Mat4, easeInOut, type EulerOrder } from "./math"
|
|
57
67
|
export type {
|
|
58
68
|
AnimationClip,
|