feat(vfx): bind effects to live animated poses
This commit is contained in:
parent
96ddfdf175
commit
542dcfc384
41 changed files with 3246 additions and 741 deletions
|
|
@ -6,6 +6,12 @@ using DatReaderWriter.Types;
|
|||
|
||||
namespace AcDream.Core.Meshing;
|
||||
|
||||
/// <summary>Final child root and indexed part poses in the parent-root frame.</summary>
|
||||
public readonly record struct EquippedChildPose(
|
||||
Matrix4x4 RootLocal,
|
||||
Matrix4x4[] PartLocal,
|
||||
MeshRef[] AttachedParts);
|
||||
|
||||
/// <summary>
|
||||
/// Retail held-object transform composition. A weapon is a separate child
|
||||
/// physics object: the parent's <see cref="Setup.HoldingLocations"/> selects
|
||||
|
|
@ -28,6 +34,90 @@ public static class EquippedChildAttachment
|
|||
IReadOnlyList<MeshRef> childPartTemplate,
|
||||
float childScale,
|
||||
out IReadOnlyList<MeshRef> attachedParts)
|
||||
{
|
||||
bool composed = TryComposePose(
|
||||
parentSetup,
|
||||
currentParentPose,
|
||||
childSetup,
|
||||
parentLocation,
|
||||
placement,
|
||||
childPartTemplate,
|
||||
childScale,
|
||||
out EquippedChildPose pose);
|
||||
attachedParts = composed ? pose.AttachedParts : Array.Empty<MeshRef>();
|
||||
return composed;
|
||||
}
|
||||
|
||||
public static bool TryComposePose(
|
||||
Setup parentSetup,
|
||||
IReadOnlyList<MeshRef> currentParentPose,
|
||||
Setup childSetup,
|
||||
ParentLocation parentLocation,
|
||||
Placement placement,
|
||||
IReadOnlyList<MeshRef> childPartTemplate,
|
||||
float childScale,
|
||||
out EquippedChildPose pose)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(currentParentPose);
|
||||
var parentParts = new Matrix4x4[currentParentPose.Count];
|
||||
for (int i = 0; i < parentParts.Length; i++)
|
||||
parentParts[i] = currentParentPose[i].PartTransform;
|
||||
return TryComposePose(
|
||||
parentSetup,
|
||||
parentParts,
|
||||
childSetup,
|
||||
parentLocation,
|
||||
placement,
|
||||
childPartTemplate,
|
||||
childScale,
|
||||
out pose);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compose from the parent's indexed final animation poses. The renderer
|
||||
/// exposes these independently of drawable MeshRefs so a hidden or missing
|
||||
/// visual part cannot shift the retail holding-location index.
|
||||
/// </summary>
|
||||
public static bool TryComposePose(
|
||||
Setup parentSetup,
|
||||
IReadOnlyList<Matrix4x4> currentParentPose,
|
||||
Setup childSetup,
|
||||
ParentLocation parentLocation,
|
||||
Placement placement,
|
||||
IReadOnlyList<MeshRef> childPartTemplate,
|
||||
float childScale,
|
||||
out EquippedChildPose pose)
|
||||
{
|
||||
return TryComposePoseInto(
|
||||
parentSetup,
|
||||
currentParentPose,
|
||||
parentPartAvailability: null,
|
||||
childSetup,
|
||||
parentLocation,
|
||||
placement,
|
||||
childPartTemplate,
|
||||
childScale,
|
||||
partPoseBuffer: null,
|
||||
attachedPartBuffer: null,
|
||||
out pose);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Allocation-free update path for an already realized child. Buffers are
|
||||
/// reused by the App-layer attachment owner after their first composition.
|
||||
/// </summary>
|
||||
public static bool TryComposePoseInto(
|
||||
Setup parentSetup,
|
||||
IReadOnlyList<Matrix4x4> currentParentPose,
|
||||
IReadOnlyList<bool>? parentPartAvailability,
|
||||
Setup childSetup,
|
||||
ParentLocation parentLocation,
|
||||
Placement placement,
|
||||
IReadOnlyList<MeshRef> childPartTemplate,
|
||||
float childScale,
|
||||
Matrix4x4[]? partPoseBuffer,
|
||||
MeshRef[]? attachedPartBuffer,
|
||||
out EquippedChildPose pose)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(parentSetup);
|
||||
ArgumentNullException.ThrowIfNull(currentParentPose);
|
||||
|
|
@ -36,14 +126,32 @@ public static class EquippedChildAttachment
|
|||
|
||||
if (!parentSetup.HoldingLocations.TryGetValue(parentLocation, out LocationType? holding))
|
||||
{
|
||||
attachedParts = Array.Empty<MeshRef>();
|
||||
pose = new EquippedChildPose(
|
||||
Matrix4x4.Identity,
|
||||
Array.Empty<Matrix4x4>(),
|
||||
Array.Empty<MeshRef>());
|
||||
return false;
|
||||
}
|
||||
|
||||
Matrix4x4 parentPart = holding.PartId >= 0
|
||||
&& holding.PartId < currentParentPose.Count
|
||||
? currentParentPose[holding.PartId].PartTransform
|
||||
: Matrix4x4.Identity;
|
||||
Matrix4x4 parentPart;
|
||||
if (holding.PartId >= 0 && holding.PartId < currentParentPose.Count)
|
||||
{
|
||||
if (parentPartAvailability is not null
|
||||
&& (holding.PartId >= parentPartAvailability.Count
|
||||
|| !parentPartAvailability[(int)holding.PartId]))
|
||||
{
|
||||
pose = default;
|
||||
return false;
|
||||
}
|
||||
parentPart = currentParentPose[(int)holding.PartId];
|
||||
}
|
||||
else
|
||||
{
|
||||
// Retail CPhysicsObj::UpdateChild (0x00512D50) uses the parent
|
||||
// root for every part number >= num_parts. This includes the
|
||||
// canonical 0xFFFFFFFF root sentinel and malformed positives.
|
||||
parentPart = Matrix4x4.Identity;
|
||||
}
|
||||
Matrix4x4 holdingFrame = ToMatrix(holding.Frame);
|
||||
Matrix4x4 childRoot = holdingFrame * parentPart;
|
||||
|
||||
|
|
@ -54,28 +162,40 @@ public static class EquippedChildAttachment
|
|||
childSetup.PlacementFrames.TryGetValue(Placement.Default, out placementFrame);
|
||||
|
||||
int partCount = Math.Min(childSetup.Parts.Count, childPartTemplate.Count);
|
||||
var result = new MeshRef[partCount];
|
||||
MeshRef[] result = attachedPartBuffer is { Length: var attachedLength }
|
||||
&& attachedLength == partCount
|
||||
? attachedPartBuffer
|
||||
: new MeshRef[partCount];
|
||||
Matrix4x4[] childPartPoses = partPoseBuffer is { Length: var poseLength }
|
||||
&& poseLength == partCount
|
||||
? partPoseBuffer
|
||||
: new Matrix4x4[partCount];
|
||||
for (int i = 0; i < partCount; i++)
|
||||
{
|
||||
Frame partFrame = placementFrame is not null && i < placementFrame.Frames.Count
|
||||
? placementFrame.Frames[i]
|
||||
: new Frame { Orientation = Quaternion.Identity };
|
||||
Vector3 scale = i < childSetup.DefaultScale.Count
|
||||
Vector3 defaultScale = i < childSetup.DefaultScale.Count
|
||||
? childSetup.DefaultScale[i]
|
||||
: Vector3.One;
|
||||
Matrix4x4 childPart = Matrix4x4.CreateScale(scale)
|
||||
Matrix4x4 visualChildPart = Matrix4x4.CreateScale(defaultScale)
|
||||
* ToMatrix(partFrame);
|
||||
if (childScale != 1.0f)
|
||||
childPart *= Matrix4x4.CreateScale(childScale);
|
||||
visualChildPart *= Matrix4x4.CreateScale(childScale);
|
||||
|
||||
Matrix4x4 rigidChildPart = Matrix4x4.CreateFromQuaternion(partFrame.Orientation)
|
||||
* Matrix4x4.CreateTranslation(partFrame.Origin * childScale);
|
||||
|
||||
childPartPoses[i] = rigidChildPart;
|
||||
|
||||
MeshRef template = childPartTemplate[i];
|
||||
result[i] = new MeshRef(template.GfxObjId, childPart * childRoot)
|
||||
result[i] = new MeshRef(template.GfxObjId, visualChildPart * childRoot)
|
||||
{
|
||||
SurfaceOverrides = template.SurfaceOverrides,
|
||||
};
|
||||
}
|
||||
|
||||
attachedParts = result;
|
||||
pose = new EquippedChildPose(childRoot, childPartPoses, result);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,48 +7,35 @@ using DatReaderWriter.Types;
|
|||
namespace AcDream.Core.Meshing;
|
||||
|
||||
/// <summary>
|
||||
/// Compute the per-part static transforms for a Setup using its
|
||||
/// PlacementFrames. For each part <c>i</c>, the returned matrix takes a
|
||||
/// point in part-local space and yields a point in setup-local space at
|
||||
/// the Setup's resting pose.
|
||||
///
|
||||
/// <para>
|
||||
/// Mirrors <see cref="SetupMesh.Flatten"/>'s pose-source priority —
|
||||
/// <c>PlacementFrames[Resting]</c> → <c>[Default]</c> → first available
|
||||
/// — so that a particle anchor at part <c>i</c> matches the part's
|
||||
/// visible rest position. If renderer and resolver ever drift on this
|
||||
/// priority, particles will visibly drift relative to their parent
|
||||
/// mesh; keep them in lockstep.
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
/// Returns an empty list when the Setup has no PlacementFrames. The
|
||||
/// caller (e.g. <c>ParticleHookSink.SpawnFromHook</c>) should then fall
|
||||
/// back to <see cref="Matrix4x4.Identity"/> per part, which is the
|
||||
/// pre-C.1.5b behavior.
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
/// For animated entities, per-part transforms vary per animation frame
|
||||
/// and live in <c>AnimatedEntityState</c>; a future "animated
|
||||
/// DefaultScript" path would publish those each tick via the same
|
||||
/// <c>SetEntityPartTransforms</c> seam. Out of scope here.
|
||||
/// </para>
|
||||
/// Computes retail's rigid per-part frames for attachments and effects.
|
||||
/// Visual GfxObj scale is deliberately excluded.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Pose selection matches <see cref="SetupMesh.Flatten"/>: an explicit motion
|
||||
/// frame, then Resting, Default, then the first placement frame. Retail
|
||||
/// <c>CPartArray::UpdateParts</c> (<c>0x005190F0</c>) scales only the frame
|
||||
/// origin by the object's scale. <c>CPartArray::SetScaleInternal</c>
|
||||
/// (<c>0x00518A00</c>) stores Setup DefaultScale separately on the visual
|
||||
/// <c>CPhysicsPart::gfxobj_scale</c>, so it never enters this rigid frame.
|
||||
/// </remarks>
|
||||
public static class SetupPartTransforms
|
||||
{
|
||||
public static IReadOnlyList<Matrix4x4> Compute(Setup setup)
|
||||
public static IReadOnlyList<Matrix4x4> Compute(
|
||||
Setup setup,
|
||||
AnimationFrame? motionFrameOverride = null,
|
||||
float objectScale = 1.0f)
|
||||
{
|
||||
AnimationFrame? source = null;
|
||||
if (setup.PlacementFrames.TryGetValue(Placement.Resting, out var resting))
|
||||
ArgumentNullException.ThrowIfNull(setup);
|
||||
AnimationFrame? source = motionFrameOverride;
|
||||
if (source is null && setup.PlacementFrames.TryGetValue(Placement.Resting, out var resting))
|
||||
{
|
||||
source = resting;
|
||||
}
|
||||
else if (setup.PlacementFrames.TryGetValue(Placement.Default, out var def))
|
||||
else if (source is null && setup.PlacementFrames.TryGetValue(Placement.Default, out var def))
|
||||
{
|
||||
source = def;
|
||||
}
|
||||
else
|
||||
else if (source is null)
|
||||
{
|
||||
foreach (var kvp in setup.PlacementFrames)
|
||||
{
|
||||
|
|
@ -57,7 +44,8 @@ public static class SetupPartTransforms
|
|||
}
|
||||
}
|
||||
|
||||
if (source is null) return System.Array.Empty<Matrix4x4>();
|
||||
if (source is null)
|
||||
return Array.Empty<Matrix4x4>();
|
||||
|
||||
int partCount = setup.Parts.Count;
|
||||
var result = new Matrix4x4[partCount];
|
||||
|
|
@ -66,10 +54,8 @@ public static class SetupPartTransforms
|
|||
Frame frame = i < source.Frames.Count
|
||||
? source.Frames[i]
|
||||
: new Frame { Origin = Vector3.Zero, Orientation = Quaternion.Identity };
|
||||
Vector3 scale = i < setup.DefaultScale.Count ? setup.DefaultScale[i] : Vector3.One;
|
||||
result[i] = Matrix4x4.CreateScale(scale)
|
||||
* Matrix4x4.CreateFromQuaternion(frame.Orientation)
|
||||
* Matrix4x4.CreateTranslation(frame.Origin);
|
||||
result[i] = Matrix4x4.CreateFromQuaternion(frame.Orientation)
|
||||
* Matrix4x4.CreateTranslation(frame.Origin * objectScale);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue