fix(vfx #56): ParticleHookSink applies CreateParticleHook.PartIndex transform
Adds per-entity part-transform side-table mirroring _rotationByEntity. SpawnFromHook now transforms the hook offset through partTransforms[partIndex] before rotating to world space. Backwards-compatible: entities without registered part transforms fall through to identity (pre-C.1.5b behavior), so the existing C.1.5a rotation-seed test stays green. Adds SetEntityPartTransforms public method. Cleared on StopAllForEntity alongside the rotation entry. 2 new xUnit tests: - SpawnFromHook_AppliesPartTransform_WhenRegistered — part 1 lifted +Z=1, hook offset (1,0,0), PartIndex=1 → world (1,0,1). - SpawnFromHook_FallsBackToIdentity_WhenPartIndexOutOfBounds — PartIndex=99 on a 2-part array → offset applied without crash, pre-C.1.5b behavior. Closes the renderer side of #56. EntityScriptActivator wiring (Task 3) lands next. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
f3bc15ed9d
commit
11521f4418
2 changed files with 115 additions and 4 deletions
|
|
@ -72,6 +72,14 @@ public sealed class ParticleHookSink : IAnimationHookSink
|
|||
private readonly ConcurrentDictionary<int, (uint EntityId, uint KeyId)> _trackingByHandle = new();
|
||||
private readonly ConcurrentDictionary<uint, ParticleRenderPass> _renderPassByEntity = new();
|
||||
private readonly ConcurrentDictionary<uint, Quaternion> _rotationByEntity = new();
|
||||
// C.1.5b #56: per-entity static part transforms (PlacementFrames[Resting]
|
||||
// baked into a Matrix4x4 per Setup part). When set, SpawnFromHook applies
|
||||
// partTransforms[hook.PartIndex] to the hook offset BEFORE rotating to
|
||||
// world space. Without this, every emitter in a multi-part Setup
|
||||
// collapses to the entity root (the bug). Cleared by StopAllForEntity.
|
||||
// For ANIMATED entities this map would need a per-tick refresh similar
|
||||
// to UpdateEntityAnchor — deferred to a future phase.
|
||||
private readonly ConcurrentDictionary<uint, IReadOnlyList<Matrix4x4>> _partTransformsByEntity = new();
|
||||
private int _anonymousEmitterSerial;
|
||||
|
||||
public ParticleHookSink(ParticleSystem system)
|
||||
|
|
@ -131,6 +139,19 @@ public sealed class ParticleHookSink : IAnimationHookSink
|
|||
public void SetEntityRotation(uint entityId, Quaternion rotation)
|
||||
=> _rotationByEntity[entityId] = rotation;
|
||||
|
||||
/// <summary>
|
||||
/// Register per-part static transforms for an entity. The caller
|
||||
/// (typically <c>EntityScriptActivator</c>) precomputes one
|
||||
/// <see cref="Matrix4x4"/> per Setup part using
|
||||
/// <c>SetupPartTransforms.Compute</c> and pushes them here at spawn
|
||||
/// time. <see cref="SpawnFromHook"/> applies
|
||||
/// <c>partTransforms[hook.PartIndex]</c> to the hook offset BEFORE
|
||||
/// transforming to world space. Cleared on
|
||||
/// <see cref="StopAllForEntity"/>.
|
||||
/// </summary>
|
||||
public void SetEntityPartTransforms(uint entityId, IReadOnlyList<Matrix4x4> partTransforms)
|
||||
=> _partTransformsByEntity[entityId] = partTransforms;
|
||||
|
||||
public void ClearEntityRenderPass(uint entityId)
|
||||
=> _renderPassByEntity.TryRemove(entityId, out _);
|
||||
|
||||
|
|
@ -171,6 +192,7 @@ public sealed class ParticleHookSink : IAnimationHookSink
|
|||
|
||||
ClearEntityRenderPass(entityId);
|
||||
_rotationByEntity.TryRemove(entityId, out _);
|
||||
_partTransformsByEntity.TryRemove(entityId, out _);
|
||||
}
|
||||
|
||||
private void SpawnFromHook(
|
||||
|
|
@ -181,13 +203,22 @@ public sealed class ParticleHookSink : IAnimationHookSink
|
|||
int partIndex,
|
||||
uint logicalId)
|
||||
{
|
||||
// Spawn position: entity pose + hook offset. PartIndex will be
|
||||
// used when the renderer passes per-part transforms through; for
|
||||
// now, fold it into the root pos.
|
||||
// Spawn position: entity pose + hook offset, with the hook
|
||||
// offset first passed through the per-part transform when
|
||||
// available (C.1.5b #56 fix). Without the per-part transform,
|
||||
// every emitter in a multi-emitter PES script collapses to the
|
||||
// entity root — visible symptom: ground-buried portal swirls.
|
||||
var rotation = _rotationByEntity.TryGetValue(entityId, out var rot)
|
||||
? rot
|
||||
: Quaternion.Identity;
|
||||
var anchor = worldPos + Vector3.Transform(offset, rotation);
|
||||
Vector3 partLocal = offset;
|
||||
if (_partTransformsByEntity.TryGetValue(entityId, out var partTransforms)
|
||||
&& partIndex >= 0
|
||||
&& partIndex < partTransforms.Count)
|
||||
{
|
||||
partLocal = Vector3.Transform(offset, partTransforms[partIndex]);
|
||||
}
|
||||
var anchor = worldPos + Vector3.Transform(partLocal, rotation);
|
||||
var renderPass = _renderPassByEntity.TryGetValue(entityId, out var pass)
|
||||
? pass
|
||||
: ParticleRenderPass.Scene;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue