reze-engine 0.55.2 → 0.55.4
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 +94 -0
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +184 -13
- 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/lyrics-api.d.ts +7 -4
- package/dist/shaders/lyrics-api.d.ts.map +1 -1
- package/dist/shaders/lyrics-api.js +15 -5
- package/package.json +1 -1
- package/src/engine.ts +224 -12
- package/src/index.ts +1 -0
- package/src/model.ts +71 -8
- package/src/shaders/lyrics-api.ts +14 -5
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. */
|
|
@@ -4970,7 +5000,7 @@ export class Engine {
|
|
|
4970
5000
|
for (const inst of this.modelInstances.values()) {
|
|
4971
5001
|
// Neither a stage nor a plane is a performer, so neither is a subject an
|
|
4972
5002
|
// effect can follow.
|
|
4973
|
-
if (!inst.model.visible || inst.isStage || inst.isPlane) continue
|
|
5003
|
+
if (!inst.model.visible || inst.isStage || inst.isPlane || inst.isProp) continue
|
|
4974
5004
|
const model = inst.model
|
|
4975
5005
|
const matrices = model.getWorldMatrices()
|
|
4976
5006
|
if (matrices.length === 0) continue
|
|
@@ -8008,7 +8038,7 @@ export class Engine {
|
|
|
8008
8038
|
// is first in insertion order and was seeding this clock with its own
|
|
8009
8039
|
// permanent zero. In a scene with a stage, a camera VMD therefore sampled
|
|
8010
8040
|
// frame 0 forever and the shot never moved.
|
|
8011
|
-
if (inst.isStage || inst.isPlane) continue
|
|
8041
|
+
if (inst.isStage || inst.isPlane || inst.isProp) continue
|
|
8012
8042
|
const p = inst.model.getAnimationProgress()
|
|
8013
8043
|
if (p.playing || p.paused) return p.current
|
|
8014
8044
|
// Otherwise the first cast member that actually HAS a clip: one still at
|
|
@@ -8460,6 +8490,16 @@ export class Engine {
|
|
|
8460
8490
|
return model
|
|
8461
8491
|
}
|
|
8462
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
|
+
|
|
8463
8503
|
/** Read a PMX out of a picked folder / expanded zip. Shared by loadModel and
|
|
8464
8504
|
* loadStage so the file-map and path handling exist in exactly one place. */
|
|
8465
8505
|
private async openPmxFromFiles(
|
|
@@ -8485,7 +8525,7 @@ export class Engine {
|
|
|
8485
8525
|
pmxPath: string,
|
|
8486
8526
|
name?: string,
|
|
8487
8527
|
assetReader?: AssetReader,
|
|
8488
|
-
options?: { stage?: boolean; plane?: boolean; dynamic?: boolean },
|
|
8528
|
+
options?: { stage?: boolean; plane?: boolean; dynamic?: boolean; prop?: boolean },
|
|
8489
8529
|
): Promise<string> {
|
|
8490
8530
|
const requested = name ?? model.name
|
|
8491
8531
|
let key = requested
|
|
@@ -8504,6 +8544,7 @@ export class Engine {
|
|
|
8504
8544
|
options?.stage ?? false,
|
|
8505
8545
|
options?.plane ?? false,
|
|
8506
8546
|
options?.dynamic ?? false,
|
|
8547
|
+
options?.prop ?? false,
|
|
8507
8548
|
)
|
|
8508
8549
|
return key
|
|
8509
8550
|
}
|
|
@@ -8537,6 +8578,28 @@ export class Engine {
|
|
|
8537
8578
|
return key
|
|
8538
8579
|
}
|
|
8539
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
|
+
|
|
8540
8603
|
/**
|
|
8541
8604
|
* Put a picture in the scene as a flat card.
|
|
8542
8605
|
*
|
|
@@ -8792,8 +8855,14 @@ export class Engine {
|
|
|
8792
8855
|
// Per-group StyleUniforms buffers aren't in gpuBuffers (allocated post-load).
|
|
8793
8856
|
for (const install of inst.styleGroups.values()) this.destroyInstall(install)
|
|
8794
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
|
+
}
|
|
8795
8863
|
this.cullListDirty = true
|
|
8796
8864
|
this.bundlesDirty = true
|
|
8865
|
+
this.updateOrderDirty = true
|
|
8797
8866
|
}
|
|
8798
8867
|
|
|
8799
8868
|
getModelNames(): string[] {
|
|
@@ -8804,6 +8873,136 @@ export class Engine {
|
|
|
8804
8873
|
return this.modelInstances.get(name)?.model ?? null
|
|
8805
8874
|
}
|
|
8806
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
|
+
|
|
8807
9006
|
/**
|
|
8808
9007
|
* Place a model in the scene — position, rotation, uniform scale, visibility. The
|
|
8809
9008
|
* transform is a root offset baked into skinning (moves the whole rig), so it composes
|
|
@@ -8815,8 +9014,11 @@ export class Engine {
|
|
|
8815
9014
|
const inst = this.modelInstances.get(name)
|
|
8816
9015
|
const model = inst?.model
|
|
8817
9016
|
if (!inst || !model) return
|
|
8818
|
-
|
|
8819
|
-
|
|
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)
|
|
8820
9022
|
if (transform.scale !== undefined) model.setScale(transform.scale)
|
|
8821
9023
|
if (transform.visible !== undefined) model.setVisible(transform.visible)
|
|
8822
9024
|
// The root transform is baked into the skin matrices, so moving a model is a
|
|
@@ -8937,7 +9139,7 @@ export class Engine {
|
|
|
8937
9139
|
|
|
8938
9140
|
for (const inst of this.modelInstances.values()) {
|
|
8939
9141
|
if (options.modelName !== undefined && inst.name !== options.modelName) continue
|
|
8940
|
-
if (inst.isStage || inst.isPlane) continue
|
|
9142
|
+
if (inst.isStage || inst.isPlane || inst.isProp) continue
|
|
8941
9143
|
const bones = inst.model.getSkeleton().bones
|
|
8942
9144
|
this.bonePickScratch = boneMarkerPositions(inst.model, this.bonePickScratch)
|
|
8943
9145
|
const pos = this.bonePickScratch
|
|
@@ -9004,7 +9206,7 @@ export class Engine {
|
|
|
9004
9206
|
|
|
9005
9207
|
for (const inst of this.modelInstances.values()) {
|
|
9006
9208
|
if (options.modelName !== undefined && inst.name !== options.modelName) continue
|
|
9007
|
-
if (inst.isStage || inst.isPlane) continue
|
|
9209
|
+
if (inst.isStage || inst.isPlane || inst.isProp) continue
|
|
9008
9210
|
const model = inst.model
|
|
9009
9211
|
const { positions } = model.getGeometry()
|
|
9010
9212
|
const count = positions.length / 3
|
|
@@ -9321,12 +9523,18 @@ export class Engine {
|
|
|
9321
9523
|
private updateInstances(deltaTime: number): void {
|
|
9322
9524
|
let animMs = 0
|
|
9323
9525
|
let physicsMs = 0
|
|
9324
|
-
this.
|
|
9526
|
+
for (const inst of this.instancesInUpdateOrder()) {
|
|
9325
9527
|
const tAnim = performance.now()
|
|
9528
|
+
// An attached model is placed from its parent's bone as posed and
|
|
9529
|
+
// simulated THIS frame — the order guarantees the parent came first —
|
|
9530
|
+
// and only then posed itself, so its clip and physics ride the placement.
|
|
9531
|
+
const attached = inst.parent !== null
|
|
9532
|
+
if (attached) this.placeAttached(inst)
|
|
9326
9533
|
// A stage never solves IK — nothing drives its chains — and skips the pose
|
|
9327
9534
|
// pass entirely while it is idle. Morph changes still come through, since
|
|
9328
|
-
// that is the one thing a stage's controls do move.
|
|
9329
|
-
|
|
9535
|
+
// that is the one thing a stage's controls do move. A prop idles the same
|
|
9536
|
+
// way while it stands on its own; hung from a hand it moves every frame.
|
|
9537
|
+
const stageIdle = (inst.isStage || inst.isPlane || inst.isProp) && !attached && inst.model.isIdle()
|
|
9330
9538
|
let verticesChanged = false
|
|
9331
9539
|
if (!stageIdle) {
|
|
9332
9540
|
verticesChanged = inst.model.update(deltaTime, inst.isStage || inst.isPlane ? false : this.ikEnabled)
|
|
@@ -9370,7 +9578,7 @@ export class Engine {
|
|
|
9370
9578
|
physicsMs += performance.now() - tPhys
|
|
9371
9579
|
}
|
|
9372
9580
|
if (inst.vertexBufferNeedsUpdate) this.updateVertexBuffer(inst)
|
|
9373
|
-
}
|
|
9581
|
+
}
|
|
9374
9582
|
this.frameAnimMsRaw = animMs
|
|
9375
9583
|
this.framePhysicsMsRaw = physicsMs
|
|
9376
9584
|
const EMA = 0.1
|
|
@@ -10327,6 +10535,7 @@ export class Engine {
|
|
|
10327
10535
|
isStage = false,
|
|
10328
10536
|
isPlane = false,
|
|
10329
10537
|
dynamicTexture = false,
|
|
10538
|
+
isProp = false,
|
|
10330
10539
|
): Promise<void> {
|
|
10331
10540
|
const vertices = model.getVertices()
|
|
10332
10541
|
const skinning = model.getSkinning()
|
|
@@ -10472,6 +10681,8 @@ export class Engine {
|
|
|
10472
10681
|
pickDrawCalls: [],
|
|
10473
10682
|
isStage,
|
|
10474
10683
|
isPlane,
|
|
10684
|
+
isProp,
|
|
10685
|
+
parent: null,
|
|
10475
10686
|
dynamicTexture,
|
|
10476
10687
|
// Seeded true: the bind pose has to reach the GPU once before any frame.
|
|
10477
10688
|
skinMatricesDirty: true,
|
|
@@ -10500,6 +10711,7 @@ export class Engine {
|
|
|
10500
10711
|
this.modelInstances.set(name, inst)
|
|
10501
10712
|
this.cullListDirty = true
|
|
10502
10713
|
this.bundlesDirty = true
|
|
10714
|
+
this.updateOrderDirty = true
|
|
10503
10715
|
}
|
|
10504
10716
|
|
|
10505
10717
|
// Build the per-model GPU vertex-morph state. Returns null (and leaves the model on the
|
|
@@ -13882,7 +14094,7 @@ export class Engine {
|
|
|
13882
14094
|
// serves.
|
|
13883
14095
|
let n = 0
|
|
13884
14096
|
this.forEachInstance((inst) => {
|
|
13885
|
-
if (n >= MAX_EFFECT_SUBJECTS || inst.isStage || inst.isPlane) return
|
|
14097
|
+
if (n >= MAX_EFFECT_SUBJECTS || inst.isStage || inst.isPlane || inst.isProp) return
|
|
13886
14098
|
const m = inst.model
|
|
13887
14099
|
// The model transform is only where the model was PLACED. A motion moves
|
|
13888
14100
|
// 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
|
}
|
|
@@ -56,10 +56,13 @@ export type LyricRect = [number, number, number, number]
|
|
|
56
56
|
/**
|
|
57
57
|
* Parse an .lrc file: `[mm:ss.xx]` tags (several per line share the text),
|
|
58
58
|
* an optional `[offset:±ms]` tag, blank-text tags kept as instrumental gaps'
|
|
59
|
-
* end markers. Lines come out sorted; each line's end is the next
|
|
60
|
-
* start, and the last line gets a ten-second hold.
|
|
61
|
-
*
|
|
62
|
-
*
|
|
59
|
+
* end markers. Lines come out sorted; each line's end is the next LATER
|
|
60
|
+
* stamp's start, and the last line gets a ten-second hold. Several lines on
|
|
61
|
+
* one stamp — an original and its translation, the bilingual .lrc idiom —
|
|
62
|
+
* stay in file order and share the one window, so an effect finds a
|
|
63
|
+
* translation as the consecutive lines whose start equals the live line's.
|
|
64
|
+
* The offset follows the LRC convention: positive shows lines EARLIER — the
|
|
65
|
+
* knob to turn when the words feel late against this particular rip.
|
|
63
66
|
*/
|
|
64
67
|
export function parseLRC(source: string): LyricLine[] {
|
|
65
68
|
let offset = 0
|
|
@@ -84,7 +87,13 @@ export function parseLRC(source: string): LyricLine[] {
|
|
|
84
87
|
// An empty-text stamp is an .lrc idiom for "the previous line ends here";
|
|
85
88
|
// it closes its predecessor and is not a line of its own.
|
|
86
89
|
if (stamped[i].text === "") continue
|
|
87
|
-
|
|
90
|
+
// The window closes at the next stamp that is actually LATER. A stamp
|
|
91
|
+
// shared by an original and its translation is one moment; closing the
|
|
92
|
+
// first line at the second's start gave it a zero-length window no clock
|
|
93
|
+
// ever fell inside, and only the translation was ever drawn.
|
|
94
|
+
let j = i + 1
|
|
95
|
+
while (j < stamped.length && stamped[j].start <= stamped[i].start) j++
|
|
96
|
+
const next = stamped[j]
|
|
88
97
|
lines.push({
|
|
89
98
|
start: stamped[i].start,
|
|
90
99
|
end: next ? next.start : stamped[i].start + 10,
|