feat(net): Phase 6.6 — parse UpdateMotion (0xF74C) into MotionUpdated event
Server sends UpdateMotion whenever an entity's motion state changes: NPCs starting a walk cycle, creatures switching to a combat stance, doors opening, a player waving, etc. Phase 6.1-6.4 already handles rendering different (stance, forward-command) pairs for the INITIAL CreateObject, but without this message NPCs freeze in whatever pose they spawned with and never transition to walking/fighting. Added UpdateMotion.TryParse with the same ServerMotionState the CreateObject path uses, reached via a slightly different outer layout (guid + instance seq + header'd MovementData; the MovementData starts with the 8-byte sequence/autonomous header this time rather than being preceded by a length field). Only the (stance, forward- command) pair is extracted — same subset CreateObject grabs. WorldSession dispatches MotionUpdated(guid, state) when a 0xF74C body parses successfully. The App-side wiring (guid→entity lookup and AnimatedEntity cycle swap) is intentionally deferred to a separate commit because it touches GameWindow which is currently being edited by the Phase 9.1 translucent-pass work. 89 Core.Net tests (was 83, +6 for UpdateMotion coverage). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
4752b8a528
commit
a71db90310
12 changed files with 675 additions and 45 deletions
|
|
@ -258,7 +258,7 @@ public sealed class GameWindow : IDisposable
|
|||
var gfx = _dats.Get<DatReaderWriter.DBObjs.GfxObj>(e.SourceGfxObjOrSetupId);
|
||||
if (gfx is not null)
|
||||
{
|
||||
var subMeshes = AcDream.Core.Meshing.GfxObjMesh.Build(gfx);
|
||||
var subMeshes = AcDream.Core.Meshing.GfxObjMesh.Build(gfx, _dats);
|
||||
_staticMesh.EnsureUploaded(e.SourceGfxObjOrSetupId, subMeshes);
|
||||
meshRefs.Add(new AcDream.Core.World.MeshRef(
|
||||
e.SourceGfxObjOrSetupId, System.Numerics.Matrix4x4.Identity));
|
||||
|
|
@ -274,7 +274,7 @@ public sealed class GameWindow : IDisposable
|
|||
{
|
||||
var gfx = _dats.Get<DatReaderWriter.DBObjs.GfxObj>(mr.GfxObjId);
|
||||
if (gfx is null) continue;
|
||||
var subMeshes = AcDream.Core.Meshing.GfxObjMesh.Build(gfx);
|
||||
var subMeshes = AcDream.Core.Meshing.GfxObjMesh.Build(gfx, _dats);
|
||||
_staticMesh.EnsureUploaded(mr.GfxObjId, subMeshes);
|
||||
meshRefs.Add(mr);
|
||||
}
|
||||
|
|
@ -344,7 +344,7 @@ public sealed class GameWindow : IDisposable
|
|||
var gfx = _dats.Get<DatReaderWriter.DBObjs.GfxObj>(spawn.ObjectId);
|
||||
if (gfx is not null)
|
||||
{
|
||||
var subMeshes = AcDream.Core.Meshing.GfxObjMesh.Build(gfx);
|
||||
var subMeshes = AcDream.Core.Meshing.GfxObjMesh.Build(gfx, _dats);
|
||||
_staticMesh.EnsureUploaded(spawn.ObjectId, subMeshes);
|
||||
meshRefs.Add(new AcDream.Core.World.MeshRef(spawn.ObjectId, scaleMat));
|
||||
}
|
||||
|
|
@ -359,7 +359,7 @@ public sealed class GameWindow : IDisposable
|
|||
{
|
||||
var gfx = _dats.Get<DatReaderWriter.DBObjs.GfxObj>(mr.GfxObjId);
|
||||
if (gfx is null) continue;
|
||||
var subMeshes = AcDream.Core.Meshing.GfxObjMesh.Build(gfx);
|
||||
var subMeshes = AcDream.Core.Meshing.GfxObjMesh.Build(gfx, _dats);
|
||||
_staticMesh.EnsureUploaded(mr.GfxObjId, subMeshes);
|
||||
// Compose: part's own transform, then the spawn's scale.
|
||||
meshRefs.Add(new AcDream.Core.World.MeshRef(mr.GfxObjId, mr.PartTransform * scaleMat));
|
||||
|
|
@ -439,7 +439,7 @@ public sealed class GameWindow : IDisposable
|
|||
if (environment is not null
|
||||
&& environment.Cells.TryGetValue(envCell.CellStructure, out var cellStruct))
|
||||
{
|
||||
var cellSubMeshes = AcDream.Core.Meshing.CellMesh.Build(envCell, cellStruct);
|
||||
var cellSubMeshes = AcDream.Core.Meshing.CellMesh.Build(envCell, cellStruct, _dats);
|
||||
if (cellSubMeshes.Count > 0)
|
||||
{
|
||||
// Use the EnvCell dat id as the GPU upload key. EnvCell ids
|
||||
|
|
@ -500,7 +500,7 @@ public sealed class GameWindow : IDisposable
|
|||
var gfx = _dats.Get<DatReaderWriter.DBObjs.GfxObj>(stab.Id);
|
||||
if (gfx is not null)
|
||||
{
|
||||
var subMeshes = AcDream.Core.Meshing.GfxObjMesh.Build(gfx);
|
||||
var subMeshes = AcDream.Core.Meshing.GfxObjMesh.Build(gfx, _dats);
|
||||
_staticMesh.EnsureUploaded(stab.Id, subMeshes);
|
||||
meshRefs.Add(new AcDream.Core.World.MeshRef(stab.Id, System.Numerics.Matrix4x4.Identity));
|
||||
}
|
||||
|
|
@ -515,7 +515,7 @@ public sealed class GameWindow : IDisposable
|
|||
{
|
||||
var gfx = _dats.Get<DatReaderWriter.DBObjs.GfxObj>(mr.GfxObjId);
|
||||
if (gfx is null) continue;
|
||||
var subMeshes = AcDream.Core.Meshing.GfxObjMesh.Build(gfx);
|
||||
var subMeshes = AcDream.Core.Meshing.GfxObjMesh.Build(gfx, _dats);
|
||||
_staticMesh.EnsureUploaded(mr.GfxObjId, subMeshes);
|
||||
meshRefs.Add(mr);
|
||||
}
|
||||
|
|
@ -860,7 +860,7 @@ public sealed class GameWindow : IDisposable
|
|||
var mr = parts[partIdx];
|
||||
var gfx = _dats.Get<DatReaderWriter.DBObjs.GfxObj>(mr.GfxObjId);
|
||||
if (gfx is null) continue;
|
||||
var subMeshes = AcDream.Core.Meshing.GfxObjMesh.Build(gfx);
|
||||
var subMeshes = AcDream.Core.Meshing.GfxObjMesh.Build(gfx, _dats);
|
||||
_staticMesh.EnsureUploaded(mr.GfxObjId, subMeshes);
|
||||
|
||||
IReadOnlyDictionary<uint, uint>? surfaceOverrides = null;
|
||||
|
|
|
|||
|
|
@ -46,5 +46,17 @@ public sealed class Shader : IDisposable
|
|||
_gl.UniformMatrix4(loc, 1, false, (float*)&m);
|
||||
}
|
||||
|
||||
public void SetInt(string name, int value)
|
||||
{
|
||||
int loc = _gl.GetUniformLocation(Program, name);
|
||||
_gl.Uniform1(loc, value);
|
||||
}
|
||||
|
||||
public void SetFloat(string name, float value)
|
||||
{
|
||||
int loc = _gl.GetUniformLocation(Program, name);
|
||||
_gl.Uniform1(loc, value);
|
||||
}
|
||||
|
||||
public void Dispose() => _gl.DeleteProgram(Program);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,18 @@ out vec4 fragColor;
|
|||
|
||||
uniform sampler2D uDiffuse;
|
||||
|
||||
// Phase 9.1: translucency kind — matches TranslucencyKind C# enum.
|
||||
// 0 = Opaque — depth write+test, no blend; shader never discards
|
||||
// 1 = ClipMap — alpha-key discard (doors, windows, vegetation)
|
||||
// 2 = AlphaBlend — GL blending handles compositing; do NOT discard
|
||||
// 3 = Additive — GL additive blending; do NOT discard
|
||||
// 4 = InvAlpha — GL inverted-alpha blending; do NOT discard
|
||||
//
|
||||
// Only ClipMap uses the alpha-discard path. AlphaBlend/Additive/InvAlpha
|
||||
// rely entirely on the GL blend stage — discarding low-alpha fragments
|
||||
// would make semi-transparent surfaces (portals, glows) fully invisible.
|
||||
uniform int uTranslucencyKind;
|
||||
|
||||
// Phase 3a: simple directional lighting. A single sun direction + ambient term
|
||||
// gives scenery and building faces enough differentiation to read as 3D instead
|
||||
// of looking like paper cutouts. Hardcoded for now; a later phase can route
|
||||
|
|
@ -19,8 +31,12 @@ const float DIFFUSE = 0.75;
|
|||
|
||||
void main() {
|
||||
vec4 sampled = texture(uDiffuse, vTex);
|
||||
// Alpha cutout for doors, windows, vegetation, and other alpha-keyed textures.
|
||||
if (sampled.a < 0.5) discard;
|
||||
|
||||
// Alpha cutout only for clip-map surfaces (doors, windows, vegetation).
|
||||
// Blended surface types (AlphaBlend, Additive, InvAlpha) must NOT
|
||||
// discard here — that would make every semi-transparent pixel invisible
|
||||
// before the blend stage even runs.
|
||||
if (uTranslucencyKind == 1 && sampled.a < 0.5) discard;
|
||||
|
||||
vec3 N = normalize(vWorldNormal);
|
||||
float ndotl = max(dot(N, SUN_DIR), 0.0);
|
||||
|
|
|
|||
|
|
@ -70,6 +70,9 @@ public sealed unsafe class StaticMeshRenderer : IDisposable
|
|||
Ebo = ebo,
|
||||
IndexCount = sm.Indices.Length,
|
||||
SurfaceId = sm.SurfaceId,
|
||||
// Capture translucency at upload time so the draw loop never
|
||||
// has to look it up from external state.
|
||||
Translucency = sm.Translucency,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -79,7 +82,14 @@ public sealed unsafe class StaticMeshRenderer : IDisposable
|
|||
_shader.SetMatrix4("uView", camera.View);
|
||||
_shader.SetMatrix4("uProjection", camera.Projection);
|
||||
|
||||
foreach (var entity in entities)
|
||||
// Snapshot entity list once so we iterate it twice (opaque then translucent)
|
||||
// without re-evaluating lazy enumerables.
|
||||
var entityList = entities as IReadOnlyList<WorldEntity> ?? entities.ToList();
|
||||
|
||||
// ── Pass 1: Opaque + ClipMap ──────────────────────────────────────────
|
||||
// Depth write on (default). No blending. ClipMap surfaces use the
|
||||
// alpha-discard path in the fragment shader (uTranslucencyKind == 1).
|
||||
foreach (var entity in entityList)
|
||||
{
|
||||
if (entity.MeshRefs.Count == 0)
|
||||
continue;
|
||||
|
|
@ -89,45 +99,22 @@ public sealed unsafe class StaticMeshRenderer : IDisposable
|
|||
if (!_gpuByGfxObj.TryGetValue(meshRef.GfxObjId, out var subMeshes))
|
||||
continue;
|
||||
|
||||
// model = entity root transform * per-part transform
|
||||
var entityRoot =
|
||||
Matrix4x4.CreateFromQuaternion(entity.Rotation) *
|
||||
Matrix4x4.CreateTranslation(entity.Position);
|
||||
var model = meshRef.PartTransform * entityRoot;
|
||||
|
||||
_shader.SetMatrix4("uModel", model);
|
||||
|
||||
foreach (var sub in subMeshes)
|
||||
{
|
||||
// Pick the right TextureCache path based on what
|
||||
// overrides this entity carries:
|
||||
// - Neither → plain GetOrUpload
|
||||
// - Per-part texture change only → GetOrUploadWithOrigTextureOverride
|
||||
// - Entity palette override (optionally + texture change)
|
||||
// → GetOrUploadWithPaletteOverride, which handles both
|
||||
//
|
||||
// Palette overrides are per-entity (shared across all parts);
|
||||
// texture overrides are per-part via meshRef.SurfaceOverrides.
|
||||
uint overrideOrigTex = 0;
|
||||
bool hasOrigTexOverride = meshRef.SurfaceOverrides is not null
|
||||
&& meshRef.SurfaceOverrides.TryGetValue(sub.SurfaceId, out overrideOrigTex);
|
||||
uint? origTexOverride = hasOrigTexOverride ? overrideOrigTex : (uint?)null;
|
||||
// Skip translucent sub-meshes in the first pass.
|
||||
if (sub.Translucency != TranslucencyKind.Opaque &&
|
||||
sub.Translucency != TranslucencyKind.ClipMap)
|
||||
continue;
|
||||
|
||||
uint tex;
|
||||
if (entity.PaletteOverride is not null)
|
||||
{
|
||||
tex = _textures.GetOrUploadWithPaletteOverride(
|
||||
sub.SurfaceId, origTexOverride, entity.PaletteOverride);
|
||||
}
|
||||
else if (hasOrigTexOverride)
|
||||
{
|
||||
tex = _textures.GetOrUploadWithOrigTextureOverride(sub.SurfaceId, overrideOrigTex);
|
||||
}
|
||||
else
|
||||
{
|
||||
tex = _textures.GetOrUpload(sub.SurfaceId);
|
||||
}
|
||||
_shader.SetInt("uTranslucencyKind", (int)sub.Translucency);
|
||||
|
||||
uint tex = ResolveTex(entity, meshRef, sub);
|
||||
_gl.ActiveTexture(TextureUnit.Texture0);
|
||||
_gl.BindTexture(TextureTarget.Texture2D, tex);
|
||||
|
||||
|
|
@ -136,9 +123,104 @@ public sealed unsafe class StaticMeshRenderer : IDisposable
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Pass 2: Translucent (AlphaBlend, Additive, InvAlpha) ─────────────
|
||||
// Depth test on so translucents composite correctly behind opaque geometry.
|
||||
// Depth write OFF so translucents don't occlude each other or downstream
|
||||
// opaque draws. Blend function is set per-draw based on TranslucencyKind.
|
||||
//
|
||||
// NOTE: translucent draws are NOT sorted by depth — overlapping translucent
|
||||
// surfaces can composite in the wrong order. Portal-sized billboards don't
|
||||
// overlap in practice so this is acceptable and avoids a larger refactor.
|
||||
_gl.Enable(EnableCap.Blend);
|
||||
_gl.DepthMask(false);
|
||||
|
||||
foreach (var entity in entityList)
|
||||
{
|
||||
if (entity.MeshRefs.Count == 0)
|
||||
continue;
|
||||
|
||||
foreach (var meshRef in entity.MeshRefs)
|
||||
{
|
||||
if (!_gpuByGfxObj.TryGetValue(meshRef.GfxObjId, out var subMeshes))
|
||||
continue;
|
||||
|
||||
var entityRoot =
|
||||
Matrix4x4.CreateFromQuaternion(entity.Rotation) *
|
||||
Matrix4x4.CreateTranslation(entity.Position);
|
||||
var model = meshRef.PartTransform * entityRoot;
|
||||
_shader.SetMatrix4("uModel", model);
|
||||
|
||||
foreach (var sub in subMeshes)
|
||||
{
|
||||
if (sub.Translucency == TranslucencyKind.Opaque ||
|
||||
sub.Translucency == TranslucencyKind.ClipMap)
|
||||
continue;
|
||||
|
||||
// Set per-draw blend function.
|
||||
switch (sub.Translucency)
|
||||
{
|
||||
case TranslucencyKind.Additive:
|
||||
// src*a + dst — portal swirls, glows
|
||||
_gl.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.One);
|
||||
break;
|
||||
|
||||
case TranslucencyKind.InvAlpha:
|
||||
// src*(1-a) + dst*a
|
||||
_gl.BlendFunc(BlendingFactor.OneMinusSrcAlpha, BlendingFactor.SrcAlpha);
|
||||
break;
|
||||
|
||||
default: // AlphaBlend
|
||||
// src*a + dst*(1-a)
|
||||
_gl.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.OneMinusSrcAlpha);
|
||||
break;
|
||||
}
|
||||
|
||||
_shader.SetInt("uTranslucencyKind", (int)sub.Translucency);
|
||||
|
||||
uint tex = ResolveTex(entity, meshRef, sub);
|
||||
_gl.ActiveTexture(TextureUnit.Texture0);
|
||||
_gl.BindTexture(TextureTarget.Texture2D, tex);
|
||||
|
||||
_gl.BindVertexArray(sub.Vao);
|
||||
_gl.DrawElements(PrimitiveType.Triangles, (uint)sub.IndexCount, DrawElementsType.UnsignedInt, (void*)0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Restore default GL state for subsequent renderers (terrain etc.).
|
||||
_gl.DepthMask(true);
|
||||
_gl.Disable(EnableCap.Blend);
|
||||
|
||||
_gl.BindVertexArray(0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolves the GL texture id for a sub-mesh, honouring palette and
|
||||
/// texture overrides carried on the entity and the mesh-ref.
|
||||
/// </summary>
|
||||
private uint ResolveTex(WorldEntity entity, MeshRef meshRef, SubMeshGpu sub)
|
||||
{
|
||||
uint overrideOrigTex = 0;
|
||||
bool hasOrigTexOverride = meshRef.SurfaceOverrides is not null
|
||||
&& meshRef.SurfaceOverrides.TryGetValue(sub.SurfaceId, out overrideOrigTex);
|
||||
uint? origTexOverride = hasOrigTexOverride ? overrideOrigTex : (uint?)null;
|
||||
|
||||
if (entity.PaletteOverride is not null)
|
||||
{
|
||||
return _textures.GetOrUploadWithPaletteOverride(
|
||||
sub.SurfaceId, origTexOverride, entity.PaletteOverride);
|
||||
}
|
||||
else if (hasOrigTexOverride)
|
||||
{
|
||||
return _textures.GetOrUploadWithOrigTextureOverride(sub.SurfaceId, overrideOrigTex);
|
||||
}
|
||||
else
|
||||
{
|
||||
return _textures.GetOrUpload(sub.SurfaceId);
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
foreach (var subs in _gpuByGfxObj.Values)
|
||||
|
|
@ -160,5 +242,10 @@ public sealed unsafe class StaticMeshRenderer : IDisposable
|
|||
public uint Ebo;
|
||||
public int IndexCount;
|
||||
public uint SurfaceId;
|
||||
/// <summary>
|
||||
/// Cached from GfxObjSubMesh.Translucency at upload time.
|
||||
/// Avoids any per-draw lookup into external state.
|
||||
/// </summary>
|
||||
public TranslucencyKind Translucency;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue