reze-engine 0.55.1 → 0.55.3
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 +135 -0
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +261 -21
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/model.d.ts +27 -0
- package/dist/model.d.ts.map +1 -1
- package/dist/model.js +70 -8
- package/dist/shaders/passes/ground.d.ts +5 -1
- package/dist/shaders/passes/ground.d.ts.map +1 -1
- package/dist/shaders/passes/ground.js +47 -28
- package/package.json +1 -1
- package/src/engine.ts +313 -20
- package/src/index.ts +1 -0
- package/src/model.ts +71 -8
- package/src/shaders/passes/ground.ts +48 -27
package/src/engine.ts
CHANGED
|
@@ -380,6 +380,24 @@ export type ModelTransform = {
|
|
|
380
380
|
visible: boolean
|
|
381
381
|
}
|
|
382
382
|
|
|
383
|
+
/** How a model rides another — MMD's 外部親 (outside parent). See setModelParent. */
|
|
384
|
+
export type ModelAttachment = {
|
|
385
|
+
/** Model key of the parent. */
|
|
386
|
+
model: string
|
|
387
|
+
/** Bone on the parent. A name the parent's rig lacks rides the parent's root. */
|
|
388
|
+
bone: string
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/** The attachment as the engine keeps it: the record plus the two matrices the
|
|
392
|
+
* per-frame placement needs, allocated once per attach rather than per frame. */
|
|
393
|
+
type Attachment = ModelAttachment & {
|
|
394
|
+
/** Where the child's origin sits in the bone's space (position · rotation). */
|
|
395
|
+
offsetMatrix: Float32Array
|
|
396
|
+
/** The root the child is posed under this frame. Handed to Model.setRootParent
|
|
397
|
+
* BY REFERENCE and refilled every frame; see placeAttached. */
|
|
398
|
+
rootMatrix: Float32Array
|
|
399
|
+
}
|
|
400
|
+
|
|
383
401
|
type SunOptions = {
|
|
384
402
|
/** Linear color of the sun lamp (Blender: Light > Color). */
|
|
385
403
|
color?: Vec3
|
|
@@ -778,6 +796,18 @@ interface ModelInstance {
|
|
|
778
796
|
* plane into isStage would have made adding a title graphic delete the ground.
|
|
779
797
|
*/
|
|
780
798
|
isPlane: boolean
|
|
799
|
+
/**
|
|
800
|
+
* A PROP: a PMX object a character holds or wears — a microphone, a fan, a
|
|
801
|
+
* sword. The third answer beside stage and plane. It keeps what a cast member
|
|
802
|
+
* has that scenery does not (physics, outlines, its own clip) and drops what
|
|
803
|
+
* makes one a performer: no effect subject id, no seeding of the scene clock,
|
|
804
|
+
* no bone picking. Like a card it leaves the floor alone. See addProp.
|
|
805
|
+
*/
|
|
806
|
+
isProp: boolean
|
|
807
|
+
/** Who this model hangs from, or null. Any model can: a prop by design, a
|
|
808
|
+
* card for a sign in her hand, a second character for a mascot on her
|
|
809
|
+
* shoulder. See setModelParent. */
|
|
810
|
+
parent: Attachment | null
|
|
781
811
|
/** This card's texture is rewritten every frame, so it is allocated with no
|
|
782
812
|
* mip chain — rebuilding one per frame is a pass per level per card, and is
|
|
783
813
|
* what a moving card was mostly costing. See setPlaneFrame. */
|
|
@@ -1510,6 +1540,13 @@ export class Engine {
|
|
|
1510
1540
|
// tone (bottom). Stand-in for MMD's toon01–10.bmp, which we can't ship.
|
|
1511
1541
|
private defaultToonRampTexture!: GPUTexture
|
|
1512
1542
|
private groundShadowPipeline!: GPURenderPipeline
|
|
1543
|
+
/** The soft-edge variant, built the first time a scene asks for one. Null while
|
|
1544
|
+
* no scene has, which is most of them — a pipeline nobody draws with is still
|
|
1545
|
+
* a shader compile at load. */
|
|
1546
|
+
private groundShadowSoftPipeline: GPURenderPipeline | null = null
|
|
1547
|
+
/** How the ground's own pipeline is chosen, kept beside the uniform that sets
|
|
1548
|
+
* it so the draw does not have to read the buffer back. */
|
|
1549
|
+
private groundSoft = false
|
|
1513
1550
|
private groundShadowBindGroupLayout!: GPUBindGroupLayout
|
|
1514
1551
|
private outlinePipeline!: GPURenderPipeline
|
|
1515
1552
|
private selectedMaterial: { modelName: string; materialName: string } | null = null
|
|
@@ -4963,7 +5000,7 @@ export class Engine {
|
|
|
4963
5000
|
for (const inst of this.modelInstances.values()) {
|
|
4964
5001
|
// Neither a stage nor a plane is a performer, so neither is a subject an
|
|
4965
5002
|
// effect can follow.
|
|
4966
|
-
if (!inst.model.visible || inst.isStage || inst.isPlane) continue
|
|
5003
|
+
if (!inst.model.visible || inst.isStage || inst.isPlane || inst.isProp) continue
|
|
4967
5004
|
const model = inst.model
|
|
4968
5005
|
const matrices = model.getWorldMatrices()
|
|
4969
5006
|
if (matrices.length === 0) continue
|
|
@@ -6068,14 +6105,9 @@ export class Engine {
|
|
|
6068
6105
|
{ binding: 12, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "float" } },
|
|
6069
6106
|
],
|
|
6070
6107
|
})
|
|
6071
|
-
|
|
6072
|
-
label: "ground shadow",
|
|
6073
|
-
code: groundShaderWgsl(),
|
|
6074
|
-
})
|
|
6075
|
-
this.groundShadowPipeline = this.createRenderPipeline({
|
|
6108
|
+
this.groundShadowPipelineDesc = {
|
|
6076
6109
|
label: "ground shadow pipeline",
|
|
6077
6110
|
layout: this.device.createPipelineLayout({ bindGroupLayouts: [this.groundShadowBindGroupLayout] }),
|
|
6078
|
-
shaderModule: groundShadowShader,
|
|
6079
6111
|
// Slot 0 only — the ground has no skinning, and declaring the full
|
|
6080
6112
|
// 3-slot layout while renderGround binds one buffer is a WebGPU
|
|
6081
6113
|
// validation error that invalidates the whole command buffer.
|
|
@@ -6083,7 +6115,8 @@ export class Engine {
|
|
|
6083
6115
|
fragmentTargets: sceneTargetsFor("ground", this.sceneFormats),
|
|
6084
6116
|
cullMode: "back",
|
|
6085
6117
|
depthStencil: { format: this.depthFormat, depthWriteEnabled: true, depthCompare: this.depthAhead },
|
|
6086
|
-
}
|
|
6118
|
+
}
|
|
6119
|
+
this.groundShadowPipeline = this.buildGroundPipeline(false)
|
|
6087
6120
|
|
|
6088
6121
|
// Outline: group 0 = per-frame (camera), group 1 = per-instance (skinMats), group 2 = per-material (edge uniforms)
|
|
6089
6122
|
this.outlinePerFrameBindGroupLayout = this.device.createBindGroupLayout({
|
|
@@ -8005,7 +8038,7 @@ export class Engine {
|
|
|
8005
8038
|
// is first in insertion order and was seeding this clock with its own
|
|
8006
8039
|
// permanent zero. In a scene with a stage, a camera VMD therefore sampled
|
|
8007
8040
|
// frame 0 forever and the shot never moved.
|
|
8008
|
-
if (inst.isStage || inst.isPlane) continue
|
|
8041
|
+
if (inst.isStage || inst.isPlane || inst.isProp) continue
|
|
8009
8042
|
const p = inst.model.getAnimationProgress()
|
|
8010
8043
|
if (p.playing || p.paused) return p.current
|
|
8011
8044
|
// Otherwise the first cast member that actually HAS a clip: one still at
|
|
@@ -8457,6 +8490,16 @@ export class Engine {
|
|
|
8457
8490
|
return model
|
|
8458
8491
|
}
|
|
8459
8492
|
|
|
8493
|
+
/** loadModel's folder/zip path for a prop. See addProp. */
|
|
8494
|
+
async loadProp(
|
|
8495
|
+
name: string,
|
|
8496
|
+
options: LoadModelFromFilesOptions & { transform?: Partial<ModelTransform> },
|
|
8497
|
+
): Promise<Model> {
|
|
8498
|
+
const { model, pmxKey, reader } = await this.openPmxFromFiles(name, options)
|
|
8499
|
+
await this.addProp(model, pmxKey, { name, transform: options.transform, assetReader: reader })
|
|
8500
|
+
return model
|
|
8501
|
+
}
|
|
8502
|
+
|
|
8460
8503
|
/** Read a PMX out of a picked folder / expanded zip. Shared by loadModel and
|
|
8461
8504
|
* loadStage so the file-map and path handling exist in exactly one place. */
|
|
8462
8505
|
private async openPmxFromFiles(
|
|
@@ -8482,7 +8525,7 @@ export class Engine {
|
|
|
8482
8525
|
pmxPath: string,
|
|
8483
8526
|
name?: string,
|
|
8484
8527
|
assetReader?: AssetReader,
|
|
8485
|
-
options?: { stage?: boolean; plane?: boolean; dynamic?: boolean },
|
|
8528
|
+
options?: { stage?: boolean; plane?: boolean; dynamic?: boolean; prop?: boolean },
|
|
8486
8529
|
): Promise<string> {
|
|
8487
8530
|
const requested = name ?? model.name
|
|
8488
8531
|
let key = requested
|
|
@@ -8501,6 +8544,7 @@ export class Engine {
|
|
|
8501
8544
|
options?.stage ?? false,
|
|
8502
8545
|
options?.plane ?? false,
|
|
8503
8546
|
options?.dynamic ?? false,
|
|
8547
|
+
options?.prop ?? false,
|
|
8504
8548
|
)
|
|
8505
8549
|
return key
|
|
8506
8550
|
}
|
|
@@ -8534,6 +8578,28 @@ export class Engine {
|
|
|
8534
8578
|
return key
|
|
8535
8579
|
}
|
|
8536
8580
|
|
|
8581
|
+
/**
|
|
8582
|
+
* Add a PMX as a PROP: an object a character holds or wears rather than a
|
|
8583
|
+
* performer or the environment. A microphone, a fan, a sword, an umbrella.
|
|
8584
|
+
*
|
|
8585
|
+
* It keeps what makes a held thing look right — physics (the charm on a phone
|
|
8586
|
+
* strap swings), toon outlines, its own clip if it has one — and drops what
|
|
8587
|
+
* makes a model a cast member: no effect subject id, so a silhouette effect
|
|
8588
|
+
* still outlines HER and not the mic; no seeding of the scene clock; no bone
|
|
8589
|
+
* picking in the pose editor. Like a card it leaves the built-in ground
|
|
8590
|
+
* alone, which is the one thing a stage does that a prop must not. Usually
|
|
8591
|
+
* hung from a bone with setModelParent, though it can stand on its own.
|
|
8592
|
+
*/
|
|
8593
|
+
async addProp(
|
|
8594
|
+
model: Model,
|
|
8595
|
+
pmxPath: string,
|
|
8596
|
+
options?: { name?: string; transform?: Partial<ModelTransform>; assetReader?: AssetReader },
|
|
8597
|
+
): Promise<string> {
|
|
8598
|
+
const key = await this.addModel(model, pmxPath, options?.name, options?.assetReader, { prop: true })
|
|
8599
|
+
if (options?.transform) this.setModelTransform(key, options.transform)
|
|
8600
|
+
return key
|
|
8601
|
+
}
|
|
8602
|
+
|
|
8537
8603
|
/**
|
|
8538
8604
|
* Put a picture in the scene as a flat card.
|
|
8539
8605
|
*
|
|
@@ -8789,8 +8855,14 @@ export class Engine {
|
|
|
8789
8855
|
// Per-group StyleUniforms buffers aren't in gpuBuffers (allocated post-load).
|
|
8790
8856
|
for (const install of inst.styleGroups.values()) this.destroyInstall(install)
|
|
8791
8857
|
this.modelInstances.delete(name)
|
|
8858
|
+
// Whatever hung from it stands on its own now, at identity — the same
|
|
8859
|
+
// place a detach leaves a model.
|
|
8860
|
+
for (const other of this.modelInstances.values()) {
|
|
8861
|
+
if (other.parent?.model === name) this.setModelParent(other.name, null)
|
|
8862
|
+
}
|
|
8792
8863
|
this.cullListDirty = true
|
|
8793
8864
|
this.bundlesDirty = true
|
|
8865
|
+
this.updateOrderDirty = true
|
|
8794
8866
|
}
|
|
8795
8867
|
|
|
8796
8868
|
getModelNames(): string[] {
|
|
@@ -8801,6 +8873,136 @@ export class Engine {
|
|
|
8801
8873
|
return this.modelInstances.get(name)?.model ?? null
|
|
8802
8874
|
}
|
|
8803
8875
|
|
|
8876
|
+
/**
|
|
8877
|
+
* Hang a model from a bone of another — MMD's 外部親 (outside parent).
|
|
8878
|
+
*
|
|
8879
|
+
* Every frame, after the parent has been posed and simulated, the child's
|
|
8880
|
+
* root bones are placed at that bone with `offset` composed on top, and only
|
|
8881
|
+
* then is the child posed itself. The placement enters through the child's
|
|
8882
|
+
* BONES rather than its model transform (Model.setRootParent): physics runs
|
|
8883
|
+
* in model space, so a root moved by the transform would have a charm on a
|
|
8884
|
+
* phone strap feel gravity swing with the hand, while a root moved by the
|
|
8885
|
+
* skeleton keeps down down. It also puts the child's own clip on top of the
|
|
8886
|
+
* ride, as MMD does — an umbrella that spins keeps spinning in the hand.
|
|
8887
|
+
*
|
|
8888
|
+
* While attached the child's position and rotation are held at identity and
|
|
8889
|
+
* setModelTransform ignores them; scale still applies, and is folded into
|
|
8890
|
+
* the placement so the offset stays in the parent's units. Detaching leaves
|
|
8891
|
+
* the model at identity until the host places it again.
|
|
8892
|
+
*
|
|
8893
|
+
* A bone the parent lacks rides the parent's root, which is what camera
|
|
8894
|
+
* follow does with an unknown name. Returns false for an unknown model, a
|
|
8895
|
+
* missing parent, or a model asked to ride itself.
|
|
8896
|
+
*/
|
|
8897
|
+
setModelParent(
|
|
8898
|
+
name: string,
|
|
8899
|
+
parent: string | null,
|
|
8900
|
+
bone = "全ての親",
|
|
8901
|
+
offset?: { position?: Vec3; rotation?: Quat },
|
|
8902
|
+
): boolean {
|
|
8903
|
+
const inst = this.modelInstances.get(name)
|
|
8904
|
+
if (!inst) return false
|
|
8905
|
+
if (parent === null) {
|
|
8906
|
+
if (inst.parent) {
|
|
8907
|
+
inst.parent = null
|
|
8908
|
+
inst.model.setRootParent(null)
|
|
8909
|
+
inst.skinMatricesDirty = true
|
|
8910
|
+
this.updateOrderDirty = true
|
|
8911
|
+
}
|
|
8912
|
+
return true
|
|
8913
|
+
}
|
|
8914
|
+
if (parent === name || !this.modelInstances.has(parent)) return false
|
|
8915
|
+
const p = offset?.position ?? new Vec3(0, 0, 0)
|
|
8916
|
+
const r = offset?.rotation ?? Quat.identity()
|
|
8917
|
+
const offsetMatrix = inst.parent?.offsetMatrix ?? new Float32Array(16)
|
|
8918
|
+
Mat4.fromPositionRotationScaleInto(p.x, p.y, p.z, r.x, r.y, r.z, r.w, 1, offsetMatrix)
|
|
8919
|
+
// Identity until the first frame fills it: a physics reset between now and
|
|
8920
|
+
// then re-poses the model, and a zero matrix would fold it to a point.
|
|
8921
|
+
const rootMatrix = inst.parent?.rootMatrix ?? new Float32Array([1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1])
|
|
8922
|
+
inst.parent = { model: parent, bone, offsetMatrix, rootMatrix }
|
|
8923
|
+
inst.model.setPosition(new Vec3(0, 0, 0))
|
|
8924
|
+
inst.model.setRotation(Quat.identity())
|
|
8925
|
+
inst.model.setRootParent(rootMatrix)
|
|
8926
|
+
inst.skinMatricesDirty = true
|
|
8927
|
+
this.updateOrderDirty = true
|
|
8928
|
+
return true
|
|
8929
|
+
}
|
|
8930
|
+
|
|
8931
|
+
/** What a model hangs from, or null. */
|
|
8932
|
+
getModelParent(name: string): ModelAttachment | null {
|
|
8933
|
+
const att = this.modelInstances.get(name)?.parent
|
|
8934
|
+
return att ? { model: att.model, bone: att.bone } : null
|
|
8935
|
+
}
|
|
8936
|
+
|
|
8937
|
+
/**
|
|
8938
|
+
* The root an attached model is posed under this frame: the parent's
|
|
8939
|
+
* placement, its bone as posed and simulated, then the offset.
|
|
8940
|
+
*
|
|
8941
|
+
* The translation is divided by the child's own scale. The skin bake
|
|
8942
|
+
* multiplies the child's scale back on outside the skeleton, and a uniform
|
|
8943
|
+
* scale commutes with the rotation, so this is exactly what lands the child
|
|
8944
|
+
* at the bone in world units while its mesh still comes out scaled.
|
|
8945
|
+
*/
|
|
8946
|
+
private placeAttached(inst: ModelInstance): void {
|
|
8947
|
+
const att = inst.parent!
|
|
8948
|
+
const parent = this.modelInstances.get(att.model)
|
|
8949
|
+
if (!parent) {
|
|
8950
|
+
this.setModelParent(inst.name, null)
|
|
8951
|
+
return
|
|
8952
|
+
}
|
|
8953
|
+
const out = att.rootMatrix
|
|
8954
|
+
const tmp = this.attachScratch
|
|
8955
|
+
const root = parent.model.getRootMatrix()
|
|
8956
|
+
const bone = parent.model.getBoneWorldMatrix(att.bone)
|
|
8957
|
+
if (bone) {
|
|
8958
|
+
Mat4.multiplyArrays(root, 0, bone, 0, tmp, 0)
|
|
8959
|
+
Mat4.multiplyArrays(tmp, 0, att.offsetMatrix, 0, out, 0)
|
|
8960
|
+
} else {
|
|
8961
|
+
Mat4.multiplyArrays(root, 0, att.offsetMatrix, 0, out, 0)
|
|
8962
|
+
}
|
|
8963
|
+
const s = inst.model.scale
|
|
8964
|
+
if (s > 0 && s !== 1) {
|
|
8965
|
+
const k = 1 / s
|
|
8966
|
+
out[12] *= k
|
|
8967
|
+
out[13] *= k
|
|
8968
|
+
out[14] *= k
|
|
8969
|
+
}
|
|
8970
|
+
}
|
|
8971
|
+
private readonly attachScratch = new Float32Array(16)
|
|
8972
|
+
|
|
8973
|
+
/** Instances in pose order: a parent before every model hanging from it, so
|
|
8974
|
+
* a child reads the bone as posed and simulated THIS frame. Insertion order
|
|
8975
|
+
* otherwise. Rebuilt when a model is added, removed or re-parented. */
|
|
8976
|
+
private updateOrder: ModelInstance[] = []
|
|
8977
|
+
private updateOrderDirty = true
|
|
8978
|
+
private instancesInUpdateOrder(): ModelInstance[] {
|
|
8979
|
+
if (!this.updateOrderDirty) return this.updateOrder
|
|
8980
|
+
const placed = new Set<string>()
|
|
8981
|
+
const order: ModelInstance[] = []
|
|
8982
|
+
let pending = Array.from(this.modelInstances.values())
|
|
8983
|
+
while (pending.length > 0) {
|
|
8984
|
+
const rest: ModelInstance[] = []
|
|
8985
|
+
for (const inst of pending) {
|
|
8986
|
+
const p = inst.parent?.model
|
|
8987
|
+
if (p === undefined || placed.has(p) || !this.modelInstances.has(p)) {
|
|
8988
|
+
order.push(inst)
|
|
8989
|
+
placed.add(inst.name)
|
|
8990
|
+
} else rest.push(inst)
|
|
8991
|
+
}
|
|
8992
|
+
if (rest.length === pending.length) {
|
|
8993
|
+
// A cycle: nothing left can go first. They pose in insertion order and
|
|
8994
|
+
// each reads the other's previous frame, which is the best a cycle gets.
|
|
8995
|
+
console.warn(`[reze] attachment cycle: ${rest.map((r) => r.name).join(" → ")}`)
|
|
8996
|
+
order.push(...rest)
|
|
8997
|
+
break
|
|
8998
|
+
}
|
|
8999
|
+
pending = rest
|
|
9000
|
+
}
|
|
9001
|
+
this.updateOrder = order
|
|
9002
|
+
this.updateOrderDirty = false
|
|
9003
|
+
return order
|
|
9004
|
+
}
|
|
9005
|
+
|
|
8804
9006
|
/**
|
|
8805
9007
|
* Place a model in the scene — position, rotation, uniform scale, visibility. The
|
|
8806
9008
|
* transform is a root offset baked into skinning (moves the whole rig), so it composes
|
|
@@ -8812,8 +9014,11 @@ export class Engine {
|
|
|
8812
9014
|
const inst = this.modelInstances.get(name)
|
|
8813
9015
|
const model = inst?.model
|
|
8814
9016
|
if (!inst || !model) return
|
|
8815
|
-
|
|
8816
|
-
|
|
9017
|
+
// An attached model is placed by its parent's bone; its own position and
|
|
9018
|
+
// rotation are held at identity so the ride is the whole placement (see
|
|
9019
|
+
// setModelParent). Scale and visibility are still its own.
|
|
9020
|
+
if (transform.position && !inst.parent) model.setPosition(transform.position)
|
|
9021
|
+
if (transform.rotation && !inst.parent) model.setRotation(transform.rotation)
|
|
8817
9022
|
if (transform.scale !== undefined) model.setScale(transform.scale)
|
|
8818
9023
|
if (transform.visible !== undefined) model.setVisible(transform.visible)
|
|
8819
9024
|
// The root transform is baked into the skin matrices, so moving a model is a
|
|
@@ -8934,7 +9139,7 @@ export class Engine {
|
|
|
8934
9139
|
|
|
8935
9140
|
for (const inst of this.modelInstances.values()) {
|
|
8936
9141
|
if (options.modelName !== undefined && inst.name !== options.modelName) continue
|
|
8937
|
-
if (inst.isStage || inst.isPlane) continue
|
|
9142
|
+
if (inst.isStage || inst.isPlane || inst.isProp) continue
|
|
8938
9143
|
const bones = inst.model.getSkeleton().bones
|
|
8939
9144
|
this.bonePickScratch = boneMarkerPositions(inst.model, this.bonePickScratch)
|
|
8940
9145
|
const pos = this.bonePickScratch
|
|
@@ -9001,7 +9206,7 @@ export class Engine {
|
|
|
9001
9206
|
|
|
9002
9207
|
for (const inst of this.modelInstances.values()) {
|
|
9003
9208
|
if (options.modelName !== undefined && inst.name !== options.modelName) continue
|
|
9004
|
-
if (inst.isStage || inst.isPlane) continue
|
|
9209
|
+
if (inst.isStage || inst.isPlane || inst.isProp) continue
|
|
9005
9210
|
const model = inst.model
|
|
9006
9211
|
const { positions } = model.getGeometry()
|
|
9007
9212
|
const count = positions.length / 3
|
|
@@ -9192,6 +9397,59 @@ export class Engine {
|
|
|
9192
9397
|
else inst.hiddenMaterials.add(materialName)
|
|
9193
9398
|
}
|
|
9194
9399
|
|
|
9400
|
+
/**
|
|
9401
|
+
* Push a colour/shading edit straight into a material's own uniform buffer —
|
|
9402
|
+
* the same block createMaterialUniformBuffer wrote at load, offset for
|
|
9403
|
+
* offset. A single write per call, on the fields that actually moved, so
|
|
9404
|
+
* dragging one slider does not touch the other eleven.
|
|
9405
|
+
*
|
|
9406
|
+
* UNGROUPED materials only. A grouped material renders through its style
|
|
9407
|
+
* group's own compiled graph (setupPipelines' neutral/DEFAULT_GRAPH path is
|
|
9408
|
+
* what reads this buffer, and a grouped material never runs it) — the call
|
|
9409
|
+
* still writes the bytes, they are simply never sampled, which would look
|
|
9410
|
+
* like the edit silently failing. Callers check groupsByModel first and this
|
|
9411
|
+
* quietly no-ops rather than assume that check was made, since a document
|
|
9412
|
+
* edit landing to the WRONG channel (the group's own colour input) would be
|
|
9413
|
+
* a worse failure than one that does nothing.
|
|
9414
|
+
*
|
|
9415
|
+
* Structural fields — anything that changes which draw bucket a material is
|
|
9416
|
+
* in, or which texture it binds — are NOT here: alpha crossing the 1.0
|
|
9417
|
+
* opaque/transparent line, edge on/off, a texture swap, all need the draw
|
|
9418
|
+
* list or the bind group rebuilt, not a uniform write. Those get their own
|
|
9419
|
+
* call when something needs them.
|
|
9420
|
+
*/
|
|
9421
|
+
setMaterialUniforms(
|
|
9422
|
+
modelName: string,
|
|
9423
|
+
materialName: string,
|
|
9424
|
+
patch: {
|
|
9425
|
+
diffuse?: readonly [number, number, number, number]
|
|
9426
|
+
specular?: readonly [number, number, number]
|
|
9427
|
+
specularPower?: number
|
|
9428
|
+
ambient?: readonly [number, number, number]
|
|
9429
|
+
},
|
|
9430
|
+
): boolean {
|
|
9431
|
+
const inst = this.modelInstances.get(modelName)
|
|
9432
|
+
if (!inst) return false
|
|
9433
|
+
const materials = inst.model.getMaterials()
|
|
9434
|
+
const index = materials.findIndex((m) => m.name === materialName)
|
|
9435
|
+
if (index < 0) return false
|
|
9436
|
+
const buffer = inst.materialUniformBuffers[index]
|
|
9437
|
+
if (!buffer) return false
|
|
9438
|
+
if (patch.diffuse) {
|
|
9439
|
+
this.device.queue.writeBuffer(buffer, 0, new Float32Array(patch.diffuse))
|
|
9440
|
+
}
|
|
9441
|
+
if (patch.ambient) {
|
|
9442
|
+
this.device.queue.writeBuffer(buffer, 16, new Float32Array(patch.ambient))
|
|
9443
|
+
}
|
|
9444
|
+
if (patch.specularPower !== undefined) {
|
|
9445
|
+
this.device.queue.writeBuffer(buffer, 28, new Float32Array([patch.specularPower]))
|
|
9446
|
+
}
|
|
9447
|
+
if (patch.specular) {
|
|
9448
|
+
this.device.queue.writeBuffer(buffer, 32, new Float32Array(patch.specular))
|
|
9449
|
+
}
|
|
9450
|
+
return true
|
|
9451
|
+
}
|
|
9452
|
+
|
|
9195
9453
|
isMaterialVisible(modelName: string, materialName: string): boolean {
|
|
9196
9454
|
const inst = this.modelInstances.get(modelName)
|
|
9197
9455
|
return inst ? !inst.hiddenMaterials.has(materialName) : false
|
|
@@ -9318,12 +9576,18 @@ export class Engine {
|
|
|
9318
9576
|
private updateInstances(deltaTime: number): void {
|
|
9319
9577
|
let animMs = 0
|
|
9320
9578
|
let physicsMs = 0
|
|
9321
|
-
this.
|
|
9579
|
+
for (const inst of this.instancesInUpdateOrder()) {
|
|
9322
9580
|
const tAnim = performance.now()
|
|
9581
|
+
// An attached model is placed from its parent's bone as posed and
|
|
9582
|
+
// simulated THIS frame — the order guarantees the parent came first —
|
|
9583
|
+
// and only then posed itself, so its clip and physics ride the placement.
|
|
9584
|
+
const attached = inst.parent !== null
|
|
9585
|
+
if (attached) this.placeAttached(inst)
|
|
9323
9586
|
// A stage never solves IK — nothing drives its chains — and skips the pose
|
|
9324
9587
|
// pass entirely while it is idle. Morph changes still come through, since
|
|
9325
|
-
// that is the one thing a stage's controls do move.
|
|
9326
|
-
|
|
9588
|
+
// that is the one thing a stage's controls do move. A prop idles the same
|
|
9589
|
+
// way while it stands on its own; hung from a hand it moves every frame.
|
|
9590
|
+
const stageIdle = (inst.isStage || inst.isPlane || inst.isProp) && !attached && inst.model.isIdle()
|
|
9327
9591
|
let verticesChanged = false
|
|
9328
9592
|
if (!stageIdle) {
|
|
9329
9593
|
verticesChanged = inst.model.update(deltaTime, inst.isStage || inst.isPlane ? false : this.ikEnabled)
|
|
@@ -9367,7 +9631,7 @@ export class Engine {
|
|
|
9367
9631
|
physicsMs += performance.now() - tPhys
|
|
9368
9632
|
}
|
|
9369
9633
|
if (inst.vertexBufferNeedsUpdate) this.updateVertexBuffer(inst)
|
|
9370
|
-
}
|
|
9634
|
+
}
|
|
9371
9635
|
this.frameAnimMsRaw = animMs
|
|
9372
9636
|
this.framePhysicsMsRaw = physicsMs
|
|
9373
9637
|
const EMA = 0.1
|
|
@@ -10324,6 +10588,7 @@ export class Engine {
|
|
|
10324
10588
|
isStage = false,
|
|
10325
10589
|
isPlane = false,
|
|
10326
10590
|
dynamicTexture = false,
|
|
10591
|
+
isProp = false,
|
|
10327
10592
|
): Promise<void> {
|
|
10328
10593
|
const vertices = model.getVertices()
|
|
10329
10594
|
const skinning = model.getSkinning()
|
|
@@ -10469,6 +10734,8 @@ export class Engine {
|
|
|
10469
10734
|
pickDrawCalls: [],
|
|
10470
10735
|
isStage,
|
|
10471
10736
|
isPlane,
|
|
10737
|
+
isProp,
|
|
10738
|
+
parent: null,
|
|
10472
10739
|
dynamicTexture,
|
|
10473
10740
|
// Seeded true: the bind pose has to reach the GPU once before any frame.
|
|
10474
10741
|
skinMatricesDirty: true,
|
|
@@ -10497,6 +10764,7 @@ export class Engine {
|
|
|
10497
10764
|
this.modelInstances.set(name, inst)
|
|
10498
10765
|
this.cullListDirty = true
|
|
10499
10766
|
this.bundlesDirty = true
|
|
10767
|
+
this.updateOrderDirty = true
|
|
10500
10768
|
}
|
|
10501
10769
|
|
|
10502
10770
|
// Build the per-model GPU vertex-morph state. Returns null (and leaves the model on the
|
|
@@ -10642,6 +10910,28 @@ export class Engine {
|
|
|
10642
10910
|
this.device.queue.writeBuffer(this.groundIndexBuffer, 0, indices)
|
|
10643
10911
|
}
|
|
10644
10912
|
|
|
10913
|
+
/** Everything about the ground's pipeline except which shadow variant it
|
|
10914
|
+
* compiles, so the two are built from one description and cannot drift. */
|
|
10915
|
+
private groundShadowPipelineDesc!: Omit<Parameters<Engine["createRenderPipeline"]>[0], "shaderModule">
|
|
10916
|
+
|
|
10917
|
+
private buildGroundPipeline(soft: boolean): GPURenderPipeline {
|
|
10918
|
+
return this.createRenderPipeline({
|
|
10919
|
+
...this.groundShadowPipelineDesc,
|
|
10920
|
+
label: soft ? "ground shadow pipeline (soft)" : "ground shadow pipeline",
|
|
10921
|
+
shaderModule: this.device.createShaderModule({
|
|
10922
|
+
label: soft ? "ground shadow (soft)" : "ground shadow",
|
|
10923
|
+
code: groundShaderWgsl(soft),
|
|
10924
|
+
}),
|
|
10925
|
+
})
|
|
10926
|
+
}
|
|
10927
|
+
|
|
10928
|
+
/** Built on the first frame that actually needs it. A shader compile costs
|
|
10929
|
+
* load time, and the overwhelming majority of scenes never soften a shadow. */
|
|
10930
|
+
private ensureGroundSoftPipeline(): GPURenderPipeline {
|
|
10931
|
+
if (!this.groundShadowSoftPipeline) this.groundShadowSoftPipeline = this.buildGroundPipeline(true)
|
|
10932
|
+
return this.groundShadowSoftPipeline
|
|
10933
|
+
}
|
|
10934
|
+
|
|
10645
10935
|
private createShadowGroundResources(opts: {
|
|
10646
10936
|
diffuseColor: Vec3
|
|
10647
10937
|
fadeStart: number
|
|
@@ -10699,6 +10989,9 @@ export class Engine {
|
|
|
10699
10989
|
// gb[18] — shadow edge softness. Was padding; the shader reads it as the
|
|
10700
10990
|
// Vogel disk's radius, and 0 takes the sharp nine-tap path unchanged.
|
|
10701
10991
|
gb[18] = Math.min(Math.max(shadowSoftness, 0), 1)
|
|
10992
|
+
// Which variant the draw picks. Zero is the sharp shader, which is the one
|
|
10993
|
+
// that existed before softness did.
|
|
10994
|
+
this.groundSoft = gb[18] > 0
|
|
10702
10995
|
// gb[17] — does the FAR cascade hold anything?
|
|
10703
10996
|
//
|
|
10704
10997
|
// It holds something only when a stage is loaded; that is what it exists for
|
|
@@ -11398,7 +11691,7 @@ export class Engine {
|
|
|
11398
11691
|
// hasGround is left alone: remove the stage and the ground comes back.
|
|
11399
11692
|
if (this.groundIsSuppressed()) return
|
|
11400
11693
|
if (!this.hasGround || !this.groundVertexBuffer || !this.groundIndexBuffer || !this.groundDrawCall) return
|
|
11401
|
-
pass.setPipeline(this.groundShadowPipeline)
|
|
11694
|
+
pass.setPipeline(this.groundSoft ? this.ensureGroundSoftPipeline() : this.groundShadowPipeline)
|
|
11402
11695
|
pass.setVertexBuffer(0, this.groundVertexBuffer)
|
|
11403
11696
|
pass.setIndexBuffer(this.groundIndexBuffer, "uint16")
|
|
11404
11697
|
pass.setBindGroup(0, this.groundDrawCall.bindGroup)
|
|
@@ -13854,7 +14147,7 @@ export class Engine {
|
|
|
13854
14147
|
// serves.
|
|
13855
14148
|
let n = 0
|
|
13856
14149
|
this.forEachInstance((inst) => {
|
|
13857
|
-
if (n >= MAX_EFFECT_SUBJECTS || inst.isStage || inst.isPlane) return
|
|
14150
|
+
if (n >= MAX_EFFECT_SUBJECTS || inst.isStage || inst.isPlane || inst.isProp) return
|
|
13858
14151
|
const m = inst.model
|
|
13859
14152
|
// The model transform is only where the model was PLACED. A motion moves
|
|
13860
14153
|
// the character by animating bones, so an effect anchored to the
|
package/src/index.ts
CHANGED
package/src/model.ts
CHANGED
|
@@ -315,6 +315,38 @@ export class Model {
|
|
|
315
315
|
this._visible = visible
|
|
316
316
|
}
|
|
317
317
|
|
|
318
|
+
/** Hang the rig's root bones from `matrix` (model space, column-major 16
|
|
319
|
+
* floats), or from nothing. The engine drives this every frame for an
|
|
320
|
+
* attached model; the matrix is read at the next world pass, not copied. */
|
|
321
|
+
setRootParent(matrix: Float32Array | null): void {
|
|
322
|
+
this.rootParent = matrix
|
|
323
|
+
if (matrix) {
|
|
324
|
+
const root = this.skeleton.bones.find((b) => b.parentIndex < 0)
|
|
325
|
+
this.primaryRootBind = root ? [root.bindTranslation[0], root.bindTranslation[1], root.bindTranslation[2]] : [0, 0, 0]
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
getRootParent(): Float32Array | null {
|
|
330
|
+
return this.rootParent
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/** The placement matrix (position · rotation · scale) the skin bake composes
|
|
334
|
+
* onto every bone. Rebuilt lazily, the way getSkinMatrices does it. */
|
|
335
|
+
getRootMatrix(): Float32Array {
|
|
336
|
+
this.refreshRootMatrix()
|
|
337
|
+
return this.rootMatrixValues
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
private refreshRootMatrix(): void {
|
|
341
|
+
if (!this.rootMatrixDirty) return
|
|
342
|
+
const p = this._position, r = this._rotation, s = this._scale
|
|
343
|
+
Mat4.fromPositionRotationScaleInto(p.x, p.y, p.z, r.x, r.y, r.z, r.w, s, this.rootMatrixValues)
|
|
344
|
+
this.rootIsIdentity =
|
|
345
|
+
p.x === 0 && p.y === 0 && p.z === 0 &&
|
|
346
|
+
r.x === 0 && r.y === 0 && r.z === 0 && r.w === 1 && s === 1
|
|
347
|
+
this.rootMatrixDirty = false
|
|
348
|
+
}
|
|
349
|
+
|
|
318
350
|
private vertexData: Float32Array<ArrayBuffer>
|
|
319
351
|
private baseVertexData: Float32Array<ArrayBuffer> // Original vertex data before morphing
|
|
320
352
|
private vertexCount: number
|
|
@@ -383,6 +415,21 @@ export class Model {
|
|
|
383
415
|
private rootMatrixValues: Float32Array = new Float32Array([1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,0,1])
|
|
384
416
|
private rootMatrixDirty: boolean = false
|
|
385
417
|
private rootIsIdentity: boolean = true
|
|
418
|
+
/** What every parentless bone hangs from — MMD's 外部親 (outside parent).
|
|
419
|
+
* Model space, so the pose pipeline, IK and physics all see it: a prop bound
|
|
420
|
+
* to a hand is moved by its BONES, and gravity keeps pointing down while the
|
|
421
|
+
* hand tilts. Null is the ordinary rig, rooted at the model's own origin.
|
|
422
|
+
* Written per frame by the engine from the parent's posed bone; see
|
|
423
|
+
* Engine.setModelParent. */
|
|
424
|
+
private rootParent: Float32Array | null = null
|
|
425
|
+
/** The bind position of the PRIMARY root — the first parentless bone, 全ての親
|
|
426
|
+
* by convention. Under a root parent that bone sits exactly ON the parent
|
|
427
|
+
* bone, as MMD's 外部親 does, so its bind position is taken off every root's
|
|
428
|
+
* local matrix: the primary lands at the parent, the other roots keep their
|
|
429
|
+
* layout relative to it. Without this the MODEL ORIGIN went to the parent
|
|
430
|
+
* bone, and a prop rigged with its one bone at the mesh's centre hung that
|
|
431
|
+
* far away from the hand. */
|
|
432
|
+
private primaryRootBind: [number, number, number] = [0, 0, 0]
|
|
386
433
|
|
|
387
434
|
// Cached skin matrices array to avoid allocations in getSkinMatrices
|
|
388
435
|
private skinMatricesArray?: Float32Array
|
|
@@ -984,6 +1031,14 @@ export class Model {
|
|
|
984
1031
|
return this.clipApplySuspended
|
|
985
1032
|
}
|
|
986
1033
|
|
|
1034
|
+
/** A bone's posed matrix — model space, column-major, the live array rather
|
|
1035
|
+
* than a copy. Null for a name this rig does not have. */
|
|
1036
|
+
getBoneWorldMatrix(boneName: string): Float32Array | null {
|
|
1037
|
+
const idx = this.runtimeSkeleton.nameIndex[boneName]
|
|
1038
|
+
if (idx === undefined || idx < 0) return null
|
|
1039
|
+
return this.runtimeSkeleton.worldMatrices[idx].values
|
|
1040
|
+
}
|
|
1041
|
+
|
|
987
1042
|
// World bone origin (world matrix col3); unknown name → null
|
|
988
1043
|
getBoneWorldPosition(boneName: string): Vec3 | null {
|
|
989
1044
|
const idx = this.runtimeSkeleton.nameIndex[boneName]
|
|
@@ -1238,14 +1293,7 @@ export class Model {
|
|
|
1238
1293
|
const skinMatrices = this.skinMatricesArray
|
|
1239
1294
|
|
|
1240
1295
|
// Rebuild root matrix + cache identity-shortcut flag only when pos/rot changed.
|
|
1241
|
-
|
|
1242
|
-
const p = this._position, r = this._rotation, s = this._scale
|
|
1243
|
-
Mat4.fromPositionRotationScaleInto(p.x, p.y, p.z, r.x, r.y, r.z, r.w, s, this.rootMatrixValues)
|
|
1244
|
-
this.rootIsIdentity =
|
|
1245
|
-
p.x === 0 && p.y === 0 && p.z === 0 &&
|
|
1246
|
-
r.x === 0 && r.y === 0 && r.z === 0 && r.w === 1 && s === 1
|
|
1247
|
-
this.rootMatrixDirty = false
|
|
1248
|
-
}
|
|
1296
|
+
this.refreshRootMatrix()
|
|
1249
1297
|
|
|
1250
1298
|
if (this.rootIsIdentity) {
|
|
1251
1299
|
// skinMatrix = worldMatrix × inverseBindMatrix
|
|
@@ -2644,6 +2692,12 @@ export class Model {
|
|
|
2644
2692
|
if (b.parentIndex >= 0) {
|
|
2645
2693
|
const parentMat = worldMats[b.parentIndex]
|
|
2646
2694
|
Mat4.multiplyArrays(parentMat.values, 0, localMVals, 0, worldMat.values, 0)
|
|
2695
|
+
} else if (this.rootParent) {
|
|
2696
|
+
const pr = this.primaryRootBind
|
|
2697
|
+
localMVals[12] -= pr[0]
|
|
2698
|
+
localMVals[13] -= pr[1]
|
|
2699
|
+
localMVals[14] -= pr[2]
|
|
2700
|
+
Mat4.multiplyArrays(this.rootParent, 0, localMVals, 0, worldMat.values, 0)
|
|
2647
2701
|
} else {
|
|
2648
2702
|
worldMat.values.set(localMVals)
|
|
2649
2703
|
}
|
|
@@ -2801,6 +2855,8 @@ export class Model {
|
|
|
2801
2855
|
// leaving every other bone — the simulated ones above all — untouched.
|
|
2802
2856
|
const order = subset ?? this.deformOrder
|
|
2803
2857
|
const count = subset ? subset.length : boneCount
|
|
2858
|
+
const rootParent = this.rootParent
|
|
2859
|
+
const primaryRootBind = this.primaryRootBind
|
|
2804
2860
|
const override = this.appendRotOverride
|
|
2805
2861
|
const overrideSet = this.appendRotOverrideSet
|
|
2806
2862
|
for (let k = 0; k < count; k++) {
|
|
@@ -2875,6 +2931,13 @@ export class Model {
|
|
|
2875
2931
|
if (b.parentIndex >= 0) {
|
|
2876
2932
|
const parentMat = worldMats[b.parentIndex]
|
|
2877
2933
|
Mat4.multiplyArrays(parentMat.values, 0, localMVals, 0, worldMat.values, 0)
|
|
2934
|
+
} else if (rootParent) {
|
|
2935
|
+
// The primary root's bind position comes off every root, so the primary
|
|
2936
|
+
// sits ON the parent bone. See primaryRootBind.
|
|
2937
|
+
localMVals[12] -= primaryRootBind[0]
|
|
2938
|
+
localMVals[13] -= primaryRootBind[1]
|
|
2939
|
+
localMVals[14] -= primaryRootBind[2]
|
|
2940
|
+
Mat4.multiplyArrays(rootParent, 0, localMVals, 0, worldMat.values, 0)
|
|
2878
2941
|
} else {
|
|
2879
2942
|
worldMat.values.set(localMVals)
|
|
2880
2943
|
}
|