Merge feat/blend-transparency: colorkey + per-object alpha/tint blend (graphics slice A)
Fixes the SC0000 opening's opaque grey-wall: layers now alpha-blend and honor per-surface colorkey. Hybrid arch (engine resolves RenderObject blend plan, Godot host blits). Deferred: smooth color-anim interpolation + additive/glow. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -102,12 +102,12 @@ Native handler sleep_op_0xc8 @0x420ec0 is NON-BLOCKING: it arms a timer (sleep_t
|
||||
- **evidence:** Ghidra handler 0x4227b0 (dispatch ctx[0x26c93+0x1ff]); FUN_0047e800(op1,(float)op2,(float)op3,(float)op4).
|
||||
|
||||
### 0x202 `gfx-blit-color` (gfx-blit-color, argc 5)
|
||||
- **summary:** 0x202 (handle)(x)(y)(alpha)(color) — gfx cmd-type 0xb. Handler gfx_op_0x202_blit_color @0x4228d0: blits object `handle` at (x,y) with a packed ARGB built from alpha(op4, ≥0x100→0xff, <0→FUN_0047f3e0) and color(op5, <0→FUN_0047f3e0) → FUN_0047ea00. See docs/engine-re.md gfx op-contract table.
|
||||
- **summary:** 0x202 (handle)(x)(y)(alpha)(color) — gfx cmd-type 0xb. Handler gfx_op_0x202_blit_color @0x4228d0: worker gfx_op_0x202_worker_set_color_anim @0x47ea00 sets an ANIMATED color/alpha target (obj+0x64) + anim bit; packs ARGB from alpha(op4, ≥0x100→0xff, <0→FUN_0047f3e0) and color(op5, <0→FUN_0047f3e0). C# VM (2026-07-08 blend slice): routes through GfxState.SetObjectColor → the compositor applies STATIC alpha/tint (BlendKind.Alpha); smooth color-anim interpolation deferred. See docs/engine-re.md §Blend & transparency.
|
||||
- **grounding:** source=investigation, confidence=high
|
||||
- **evidence:** Ghidra handler 0x4228d0 (dispatch ctx[0x26c93+0x202]); packs (alpha<<24|rgb) from operands 4/5, FUN_0047ea00(op1,op2,op3,packed).
|
||||
|
||||
### 0x203 `gfx-draw-color` (gfx-draw-color, argc 4)
|
||||
- **summary:** 0x203 (handle)(v)(alpha)(color) — gfx cmd-type 9. Handler gfx_op_0x203_draw_color @0x4229a0: draws object `handle` with op2 + a packed ARGB from alpha(op3)/color(op4) → FUN_0047e9b0. Sibling of 0x202 with one fewer positional arg. See docs/engine-re.md gfx op-contract table.
|
||||
- **summary:** 0x203 (handle)(v)(alpha)(color) — gfx cmd-type 9. Handler gfx_op_0x203_draw_color @0x4229a0: worker gfx_op_0x203_worker_set_color @0x47e9b0 sets a STATIC color/alpha (obj+0x60), no anim bit; packs ARGB from alpha(op3)/color(op4). Sibling of 0x202 (one fewer positional arg). C# VM (2026-07-08 blend slice): routes through GfxState.SetObjectColor → compositor applies static alpha/tint (BlendKind.Alpha). See docs/engine-re.md §Blend & transparency.
|
||||
- **grounding:** source=investigation, confidence=high
|
||||
- **evidence:** Ghidra handler 0x4229a0 (dispatch ctx[0x26c93+0x203]); packs color from operands 3/4, FUN_0047e9b0(op1,op2,packed).
|
||||
|
||||
|
||||
@@ -600,3 +600,32 @@ compositing + cold-object anchors (the thing that makes the paced opening actual
|
||||
the Ctrl `Speed` multiplier (ADV-mode-scope RE). (c) Full scene-coroutine framework (`0x7b`/`0x7c`/`0x140`)
|
||||
for interactive multi-object scenes. (d) Model `0xcd get-input-type` (name-entry interactivity, the separate
|
||||
input gap noted above).
|
||||
|
||||
### A2b — Blend & transparency (slice A) ✅ DONE (2026-07-08)
|
||||
|
||||
First of three graphics-fidelity slices (A blend/transparency, B geometry/anchors, C render-targets).
|
||||
Spec `docs/superpowers/specs/2026-07-08-blend-transparency-design.md`, plan
|
||||
`docs/superpowers/plans/2026-07-08-blend-transparency.md`, branch `feat/blend-transparency` (engine 69/69,
|
||||
sweep exit=284/STEP-LIMIT=13 parity, `SELFTEST OK`).
|
||||
|
||||
**Landed (hybrid: engine resolves, host blits):**
|
||||
- `Age.Engine/Model/BlendMath.cs` — pure colorkey match + ARGB unpack (colorkey format reversed:
|
||||
op arg 3 `<0` = none, else `0xRRGGBB` exact-match, `0` = key black; baked at surface-load, see
|
||||
engine-re.md §Blend).
|
||||
- `RenderObject` gains `Alpha`/`Tint`/`Blend` (`BlendKind` Opaque|Alpha|Additive); `GfxObject.HasColor`;
|
||||
`GfxState.SetObjectColor`; `SnapshotVisibleObjects` resolves them. `0x202`/`0x203` route through
|
||||
`SetObjectColor` (the stale "alpha deferred" trace stub is gone — alpha is now consumed).
|
||||
- Godot compositor: colorkey-baked image cache (keyed by `(path, colorKey)`), `BlitLayer` applies
|
||||
colorkey + object alpha + RGB-tint modulate, and **surfaceless colored objects fill a tint×alpha quad**
|
||||
(the fades) instead of being skipped.
|
||||
|
||||
**Result (pixels):** page-1 event-CG composites cleanly (dialogue + prompt, no opaque boxes); the opening's
|
||||
mid-fade frames now alpha-blend (dark bg + light-ray burst, then a blended dark transition) instead of the
|
||||
pre-change **full-screen opaque grey wall** that ate the CG. Verified via `--shot`/`--shot-sequence` on
|
||||
`SC0000 --boot`.
|
||||
|
||||
**Deferred (documented, NOT built — confirmed by a stalled interp RE pass, engine-re.md §Blend):**
|
||||
smooth color-animation *interpolation* (the fade ramps snap to the correct end-state rather than gliding —
|
||||
the `0x202` color channel's blit consumer + clock coupling is a dedicated dig) and **additive/glow blend**
|
||||
(`local_2c` mode 2/3; its object field isn't pinned). `BlendKind.Additive` is an unused seam. Next graphics
|
||||
slices unchanged: B (geometry/anchors — sprite *placement*) and C (render-targets).
|
||||
|
||||
34
engine/Age.Engine.Tests/BlendMathTests.cs
Normal file
34
engine/Age.Engine.Tests/BlendMathTests.cs
Normal file
@@ -0,0 +1,34 @@
|
||||
using Age.Engine.Model;
|
||||
using Xunit;
|
||||
|
||||
public class BlendMathTests
|
||||
{
|
||||
[Fact]
|
||||
public void NegativeColorKey_IsNoKey()
|
||||
{
|
||||
Assert.False(BlendMath.HasColorKey(-1));
|
||||
Assert.False(BlendMath.ColorKeyMatches(0, 0, 0, -1)); // never matches when no key
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ZeroColorKey_KeysBlack()
|
||||
{
|
||||
Assert.True(BlendMath.HasColorKey(0));
|
||||
Assert.True(BlendMath.ColorKeyMatches(0, 0, 0, 0)); // (0,0,0) == key black
|
||||
Assert.False(BlendMath.ColorKeyMatches(1, 0, 0, 0)); // near-black is NOT keyed (exact match)
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ColorKey_MatchesRgb888Bytes()
|
||||
{
|
||||
long key = 0xFF00FF; // magenta 0xRRGGBB
|
||||
Assert.True(BlendMath.ColorKeyMatches(0xFF, 0x00, 0xFF, key));
|
||||
Assert.False(BlendMath.ColorKeyMatches(0xFE, 0x00, 0xFF, key));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UnpackArgb_SplitsBytes()
|
||||
{
|
||||
Assert.Equal((0x80, 0x12, 0x34, 0x56), BlendMath.UnpackArgb(0x80123456));
|
||||
}
|
||||
}
|
||||
45
engine/Age.Engine.Tests/RenderObjectBlendTests.cs
Normal file
45
engine/Age.Engine.Tests/RenderObjectBlendTests.cs
Normal file
@@ -0,0 +1,45 @@
|
||||
using System.Linq;
|
||||
using Age.Engine.Model;
|
||||
using Xunit;
|
||||
|
||||
public class RenderObjectBlendTests
|
||||
{
|
||||
// Make a visible textured object bound to a slot that has a surface, so it appears in the snapshot.
|
||||
private static GfxState WithVisibleObject(long handle, long resId, long colorKey)
|
||||
{
|
||||
var g = new GfxState();
|
||||
g.SetSurface(1, resId, colorKey);
|
||||
g.BindDraw(handle, 1, 0, 0, 10, 10, 0, 0); // draw-texture: slot 1, 10x10 at (0,0), visible
|
||||
return g;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ObjectWithoutColor_ResolvesOpaqueWhiteTint()
|
||||
{
|
||||
var g = WithVisibleObject(0x100, resId: 5, colorKey: -1);
|
||||
var ro = g.SnapshotVisibleObjects().Single();
|
||||
Assert.Equal(255, ro.Alpha);
|
||||
Assert.Equal(0xFFFFFF, ro.Tint);
|
||||
Assert.Equal(BlendKind.Opaque, ro.Blend);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DrawColor_0x203_SetsAlphaTintAndBlend()
|
||||
{
|
||||
var g = WithVisibleObject(0x100, resId: 5, colorKey: -1);
|
||||
// emulate op 0x203: pack (alpha=0x80, color=0x102030) and mark HasColor
|
||||
g.SetObjectColor(0x100, GfxState.PackColor(0x80, 0x102030));
|
||||
var ro = g.SnapshotVisibleObjects().Single();
|
||||
Assert.Equal(0x80, ro.Alpha);
|
||||
Assert.Equal(0x102030, ro.Tint);
|
||||
Assert.Equal(BlendKind.Alpha, ro.Blend);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ColorKey_IsCarriedThrough()
|
||||
{
|
||||
var g = WithVisibleObject(0x100, resId: 5, colorKey: 0x000000); // key black
|
||||
var ro = g.SnapshotVisibleObjects().Single();
|
||||
Assert.Equal(0x000000, ro.ColorKey);
|
||||
}
|
||||
}
|
||||
6
engine/Age.Engine/Model/BlendKind.cs
Normal file
6
engine/Age.Engine/Model/BlendKind.cs
Normal file
@@ -0,0 +1,6 @@
|
||||
namespace Age.Engine.Model;
|
||||
|
||||
/// <summary>How an object's surface composites onto the canvas. Opaque = straight copy; Alpha = source-alpha
|
||||
/// blend (fades). Additive (glow/flash, native blit mode 2/3) is a documented seam — NOT implemented in the
|
||||
/// blend/transparency slice; see docs/superpowers/specs/2026-07-08-blend-transparency-design.md.</summary>
|
||||
public enum BlendKind { Opaque, Alpha, Additive }
|
||||
22
engine/Age.Engine/Model/BlendMath.cs
Normal file
22
engine/Age.Engine/Model/BlendMath.cs
Normal file
@@ -0,0 +1,22 @@
|
||||
namespace Age.Engine.Model;
|
||||
|
||||
/// <summary>Pure blend helpers shared by the engine resolution and the host blit. Colorkey semantics come
|
||||
/// from the native loader (docs/engine-re.md §"Blend & transparency"): op arg 3 < 0 = no key; otherwise
|
||||
/// the operand is 0xRRGGBB and matching (R,G,B) texels are transparent (operand 0 = key black).</summary>
|
||||
public static class BlendMath
|
||||
{
|
||||
/// <summary>A colorkey operand >= 0 is an active key; a negative operand means "no colorkey".</summary>
|
||||
public static bool HasColorKey(long colorKey) => colorKey >= 0;
|
||||
|
||||
/// <summary>True when (r,g,b) exactly equals the key's 0xRRGGBB bytes. Always false when there is no key.</summary>
|
||||
public static bool ColorKeyMatches(byte r, byte g, byte b, long colorKey)
|
||||
{
|
||||
if (!HasColorKey(colorKey)) return false;
|
||||
return r == ((colorKey >> 16) & 0xff) && g == ((colorKey >> 8) & 0xff) && b == (colorKey & 0xff);
|
||||
}
|
||||
|
||||
/// <summary>Split a 0xAARRGGBB packed color (see GfxState.PackColor) into its byte channels.</summary>
|
||||
public static (int A, int R, int G, int B) UnpackArgb(long packed)
|
||||
=> ((int)((packed >> 24) & 0xff), (int)((packed >> 16) & 0xff),
|
||||
(int)((packed >> 8) & 0xff), (int)(packed & 0xff));
|
||||
}
|
||||
@@ -16,7 +16,7 @@ public readonly record struct AnimState(bool Enabled, bool Normalized, long TX,
|
||||
/// "The full gfx render model").</summary>
|
||||
public readonly record struct RenderObject(long Handle, long SurfaceResId, long ColorKey,
|
||||
int SrcX, int SrcY, int W, int H, int DstX, int DstY,
|
||||
AnimState Anim);
|
||||
AnimState Anim, int Alpha, long Tint, BlendKind Blend);
|
||||
|
||||
/// <summary>Host-agnostic model of the AGE native gfx command-buffer (reversed in
|
||||
/// docs/engine-re.md, gfx op-contract table). One registry maps an object handle to a GfxObject — the
|
||||
@@ -31,6 +31,7 @@ public sealed class GfxState
|
||||
public (long X, long Y, long Z) V18, V24, V16c;
|
||||
public long Field64, Field68, Field6c;
|
||||
public long Color;
|
||||
public bool HasColor; // true once op 0x202/0x203 set a color/alpha modulation on this object
|
||||
// draw-texture bind (gfx_object_bind_draw): the surface to draw + its source rect + the visible flag.
|
||||
public int SourceSlot = -1;
|
||||
public (int X, int Y, int W, int H) SrcRect;
|
||||
@@ -129,6 +130,13 @@ public sealed class GfxState
|
||||
// ---- surfaces (image buffers per slot): ctx+0x52bd4[slot], from create/set-texture ----
|
||||
private readonly Dictionary<int, (long ResId, long ColorKey)> _surfaces = new();
|
||||
public void SetSurface(int slot, long resId, long colorKey) { lock (_lock) { _surfaces[slot] = (resId, colorKey); } }
|
||||
|
||||
/// <summary>Ops 0x202/0x203: record a packed 0xAARRGGBB color/alpha modulation on the object and mark it
|
||||
/// HasColor so the compositor applies alpha+tint (vs the opaque default).</summary>
|
||||
public void SetObjectColor(long handle, long packed)
|
||||
{
|
||||
lock (_lock) { var o = GetOrCreate(handle); o.Color = packed; o.HasColor = true; }
|
||||
}
|
||||
public void ClearSurface(int slot) { lock (_lock) { _surfaces[slot] = (0, 0); } } // create-texture (blank)
|
||||
|
||||
/// <summary>draw-texture bind (gfx_object_bind_draw): object <paramref name="handle"/> draws surface
|
||||
@@ -188,11 +196,20 @@ public sealed class GfxState
|
||||
var o = kv.Value;
|
||||
if (!o.Visible) continue;
|
||||
var (resId, ck) = _surfaces.TryGetValue(o.SourceSlot, out var s) ? s : (0L, 0L);
|
||||
|
||||
int alpha = 255; long tint = 0xFFFFFF; var blend = BlendKind.Opaque;
|
||||
if (o.HasColor)
|
||||
{
|
||||
var (a, r, g, b) = BlendMath.UnpackArgb(o.Color);
|
||||
alpha = a; tint = ((long)r << 16) | ((long)g << 8) | (long)b; blend = BlendKind.Alpha;
|
||||
}
|
||||
|
||||
list.Add(new RenderObject(kv.Key, resId, ck, o.SrcRect.X, o.SrcRect.Y, o.SrcRect.W, o.SrcRect.H,
|
||||
(int)o.V24.X, (int)o.V24.Y,
|
||||
new AnimState(o.AnimEnabled, o.AnimNormalized,
|
||||
o.AnimTarget.X, o.AnimTarget.Y, o.AnimTarget.Z,
|
||||
o.AnimDurationTicks, o.AnimGeneration)));
|
||||
o.AnimDurationTicks, o.AnimGeneration),
|
||||
alpha, tint, blend));
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
@@ -270,12 +270,10 @@ public sealed class VirtualMachine
|
||||
Gfx.EraseRange(Read(a[0]), Read(a[1])); return pc + 1;
|
||||
case "gfx-elem-release": // 0x1fa (handle)
|
||||
Gfx.Release(Read(a[0])); return pc + 1;
|
||||
case "gfx-blit-color": // 0x202 (handle)(x)(y)(alpha)(color) — blend deferred
|
||||
Gfx.GetOrCreate(Read(a[0])).Color = GfxState.PackColor(Read(a[3]), Read(a[4]));
|
||||
WarnAlphaDeferredOnce(); return pc + 1;
|
||||
case "gfx-draw-color": // 0x203 (handle)(v)(alpha)(color) — blend deferred
|
||||
Gfx.GetOrCreate(Read(a[0])).Color = GfxState.PackColor(Read(a[2]), Read(a[3]));
|
||||
WarnAlphaDeferredOnce(); return pc + 1;
|
||||
case "gfx-blit-color": // 0x202 (handle)(x)(y)(alpha)(color) — static alpha/tint (anim interp deferred)
|
||||
Gfx.SetObjectColor(Read(a[0]), GfxState.PackColor(Read(a[3]), Read(a[4]))); return pc + 1;
|
||||
case "gfx-draw-color": // 0x203 (handle)(v)(alpha)(color) — static alpha/tint
|
||||
Gfx.SetObjectColor(Read(a[0]), GfxState.PackColor(Read(a[2]), Read(a[3]))); return pc + 1;
|
||||
// ---- sprite transform / animation cluster (docs/engine-re.md "0x21c-0x243 ... ANIMATION") ----
|
||||
case "set-anim-transform-abs": // 0x220 (handle)(p1)(p2)(x)(y)(z) — set transform directly
|
||||
Gfx.SetAnimTransform(Read(a[0]), Read(a[1]), Read(a[2]),
|
||||
@@ -294,13 +292,4 @@ public sealed class VirtualMachine
|
||||
}
|
||||
}
|
||||
|
||||
// Colored-draw ops (0x202/0x203) store the packed color on the object now; the actual alpha/additive
|
||||
// blend in the compositor is deferred. Surface it once (not silently) via the trace sink — observe-only,
|
||||
// so parity holds. See docs/superpowers/specs/2026-07-07-gfx-command-buffer-design.md (Deferrals).
|
||||
private bool _warnedAlpha;
|
||||
private void WarnAlphaDeferredOnce()
|
||||
{
|
||||
if (_warnedAlpha) return; _warnedAlpha = true;
|
||||
_sink.Emit(TraceEvent.Stub(0x202, -1));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -231,7 +231,7 @@ public partial class Main : Godot.Control
|
||||
// engine's z-order), each blitting its live surface's rect at its position. Surfaces are cached by BMP
|
||||
// path (this runs every frame). Animated objects tween over the global anim-clock (0x238); their opacity
|
||||
// is applied by the alpha-aware BlitLayer. See docs/engine-re.md "sprite transform / ANIMATION cluster".
|
||||
private readonly System.Collections.Generic.Dictionary<string, Image?> _imgCache = new();
|
||||
private readonly System.Collections.Generic.Dictionary<(string Path, long Key), Image?> _imgCache = new();
|
||||
|
||||
// Per-handle wall-clock tween of the animation channel. The engine's clock (op 0x238) is a GLOBAL,
|
||||
// non-blocking clock; the host advances it here while the VM is parked at wait-for-input. Opacity comes
|
||||
@@ -257,10 +257,22 @@ public partial class Main : Godot.Control
|
||||
_lastClockGen = clockGen;
|
||||
foreach (var v in _vm.Gfx.SnapshotVisibleObjects()) // already ascending-handle = z-order
|
||||
{
|
||||
if (v.SurfaceResId == 0) continue; // render-target/blank surface (no file) — later phase
|
||||
float a = AlphaFor(v, clockReset, clockDur) * (v.Alpha / 255f);
|
||||
if (v.SurfaceResId == 0)
|
||||
{
|
||||
// A colored object with no bound surface = a fade/flash fill (e.g. fade-to-black). Fill its
|
||||
// rect (full-screen when it has no size, the opening's case) with the tint at alpha. Uncolored
|
||||
// surfaceless objects are render targets — still skipped (slice C).
|
||||
if (v.Blend != Age.Engine.Model.BlendKind.Opaque)
|
||||
{
|
||||
int fw = v.W > 0 ? v.W : 800, fh = v.H > 0 ? v.H : 600;
|
||||
FillQuad(v.DstX, v.DstY, fw, fh, v.Tint, a);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
var bmp = _host.ResolveResIdTexture(v.SurfaceResId);
|
||||
if (bmp == null) continue;
|
||||
BlitLayer(bmp, v.SrcX, v.SrcY, v.W, v.H, v.DstX, v.DstY, AlphaFor(v, clockReset, clockDur));
|
||||
BlitLayer(bmp, v.ColorKey, v.Tint, v.SrcX, v.SrcY, v.W, v.H, v.DstX, v.DstY, a);
|
||||
}
|
||||
_screenTex.Update(_screen);
|
||||
}
|
||||
@@ -287,44 +299,93 @@ public partial class Main : Godot.Control
|
||||
return (float)tw.CurrentA;
|
||||
}
|
||||
|
||||
private void BlitLayer(string bmpPath, int srcX, int srcY, int w, int h, int dstX, int dstY, float alpha = 1f)
|
||||
// Blit one object's surface rect. The source Image is cached per (path, colorKey): on first load, texels
|
||||
// matching the surface colorkey are made transparent (native bakes the key at load — engine-re.md §Blend).
|
||||
// tint (0xRRGGBB) modulates the texel RGB (fade-to-black uses tint=black); alpha is the object's opacity.
|
||||
private void BlitLayer(string bmpPath, long colorKey, long tint, int srcX, int srcY, int w, int h,
|
||||
int dstX, int dstY, float alpha = 1f)
|
||||
{
|
||||
if (!_imgCache.TryGetValue(bmpPath, out var src))
|
||||
var cacheKey = (bmpPath, colorKey);
|
||||
if (!_imgCache.TryGetValue(cacheKey, out var src))
|
||||
{
|
||||
src = new Image();
|
||||
if (src.LoadBmpFromBuffer(System.IO.File.ReadAllBytes(bmpPath)) != Error.Ok)
|
||||
{ GD.Print($"BMP load failed {bmpPath}"); src = null; }
|
||||
else if (src.GetFormat() != Image.Format.Rgba8) src.Convert(Image.Format.Rgba8);
|
||||
_imgCache[bmpPath] = src;
|
||||
else
|
||||
{
|
||||
if (src.GetFormat() != Image.Format.Rgba8) src.Convert(Image.Format.Rgba8);
|
||||
if (Age.Engine.Model.BlendMath.HasColorKey(colorKey)) BakeColorKey(src, colorKey);
|
||||
}
|
||||
_imgCache[cacheKey] = src;
|
||||
}
|
||||
if (src == null) return;
|
||||
// Clamp the source rect to the image; a zero/negative size falls back to the full image.
|
||||
|
||||
int sw = w > 0 ? w : src.GetWidth();
|
||||
int sh = h > 0 ? h : src.GetHeight();
|
||||
sw = System.Math.Min(sw, src.GetWidth() - srcX);
|
||||
sh = System.Math.Min(sh, src.GetHeight() - srcY);
|
||||
if (sw <= 0 || sh <= 0) return;
|
||||
if (alpha >= 0.999f) // fast opaque path (unchanged behaviour for non-animating objects)
|
||||
|
||||
bool plainOpaque = alpha >= 0.999f && tint == 0xFFFFFF && !Age.Engine.Model.BlendMath.HasColorKey(colorKey);
|
||||
if (plainOpaque) // fast path: unchanged behaviour for opaque, un-keyed, un-tinted layers
|
||||
{
|
||||
_screen.BlitRect(src, new Rect2I(srcX, srcY, sw, sh), new Vector2I(dstX, dstY));
|
||||
return;
|
||||
}
|
||||
// Alpha composite over the raw RGBA byte buffer: out = src*(sa) + dst*(1-sa), sa = srcAlpha * objAlpha.
|
||||
|
||||
int tr = (int)((tint >> 16) & 0xff), tg = (int)((tint >> 8) & 0xff), tb = (int)(tint & 0xff);
|
||||
byte[] dst = _screen.GetData(); byte[] ss = src.GetData();
|
||||
int dw = _screen.GetWidth(), sfw = src.GetWidth();
|
||||
int dw = _screen.GetWidth(), dh = _screen.GetHeight(), sfw = src.GetWidth();
|
||||
int ia = (int)(System.Math.Clamp(alpha, 0f, 1f) * 255);
|
||||
for (int y = 0; y < sh; y++)
|
||||
for (int x = 0; x < sw; x++)
|
||||
{
|
||||
int dxp = dstX + x, dyp = dstY + y;
|
||||
if (dxp < 0 || dyp < 0 || dxp >= dw || dyp >= _screen.GetHeight()) continue;
|
||||
if (dxp < 0 || dyp < 0 || dxp >= dw || dyp >= dh) continue;
|
||||
int di = (dyp * dw + dxp) * 4;
|
||||
int si = ((srcY + y) * sfw + (srcX + x)) * 4;
|
||||
int sa = ss[si + 3] * ia / 255;
|
||||
for (int c = 0; c < 3; c++) dst[di + c] = (byte)((ss[si + c] * sa + dst[di + c] * (255 - sa)) / 255);
|
||||
int sa = ss[si + 3] * ia / 255; // texel alpha (colorkey already 0) × object alpha
|
||||
if (sa == 0) continue;
|
||||
int sr = ss[si] * tr / 255, sg = ss[si + 1] * tg / 255, sb = ss[si + 2] * tb / 255; // tint modulate
|
||||
dst[di] = (byte)((sr * sa + dst[di] * (255 - sa)) / 255);
|
||||
dst[di + 1] = (byte)((sg * sa + dst[di + 1] * (255 - sa)) / 255);
|
||||
dst[di + 2] = (byte)((sb * sa + dst[di + 2] * (255 - sa)) / 255);
|
||||
dst[di + 3] = (byte)System.Math.Min(255, dst[di + 3] + sa);
|
||||
}
|
||||
_screen.SetData(dw, _screen.GetHeight(), false, _screen.GetFormat(), dst);
|
||||
_screen.SetData(dw, dh, false, _screen.GetFormat(), dst);
|
||||
}
|
||||
|
||||
// Alpha-blend a solid tint (0xRRGGBB) rectangle over the screen — the surfaceless fade/flash fill.
|
||||
private void FillQuad(int dstX, int dstY, int w, int h, long tint, float alpha)
|
||||
{
|
||||
int ia = (int)(System.Math.Clamp(alpha, 0f, 1f) * 255);
|
||||
if (ia == 0) return;
|
||||
int tr = (int)((tint >> 16) & 0xff), tg = (int)((tint >> 8) & 0xff), tb = (int)(tint & 0xff);
|
||||
byte[] dst = _screen.GetData();
|
||||
int dw = _screen.GetWidth(), dh = _screen.GetHeight();
|
||||
for (int y = 0; y < h; y++)
|
||||
for (int x = 0; x < w; x++)
|
||||
{
|
||||
int dxp = dstX + x, dyp = dstY + y;
|
||||
if (dxp < 0 || dyp < 0 || dxp >= dw || dyp >= dh) continue;
|
||||
int di = (dyp * dw + dxp) * 4;
|
||||
dst[di] = (byte)((tr * ia + dst[di] * (255 - ia)) / 255);
|
||||
dst[di + 1] = (byte)((tg * ia + dst[di + 1] * (255 - ia)) / 255);
|
||||
dst[di + 2] = (byte)((tb * ia + dst[di + 2] * (255 - ia)) / 255);
|
||||
dst[di + 3] = (byte)System.Math.Min(255, dst[di + 3] + ia);
|
||||
}
|
||||
_screen.SetData(dw, dh, false, _screen.GetFormat(), dst);
|
||||
}
|
||||
|
||||
// Make colorkey-matching texels transparent (native colorkey is baked at surface load).
|
||||
private static void BakeColorKey(Image img, long colorKey)
|
||||
{
|
||||
byte[] px = img.GetData();
|
||||
int w = img.GetWidth(), h = img.GetHeight();
|
||||
for (int i = 0; i < px.Length; i += 4)
|
||||
if (Age.Engine.Model.BlendMath.ColorKeyMatches(px[i], px[i + 1], px[i + 2], colorKey))
|
||||
px[i + 3] = 0;
|
||||
img.SetData(w, h, false, img.GetFormat(), px);
|
||||
}
|
||||
|
||||
// Load an OGG off disk and play it. BGM loops; voice plays once, cutting off any prior line.
|
||||
|
||||
@@ -4791,7 +4791,7 @@ abi_source = "kelebek+decode-validated"
|
||||
[opcode.semantics]
|
||||
name = "gfx-blit-color"
|
||||
category = "draw"
|
||||
summary = "0x202 (handle)(x)(y)(alpha)(color) — gfx cmd-type 0xb. Handler gfx_op_0x202_blit_color @0x4228d0: blits object `handle` at (x,y) with a packed ARGB built from alpha(op4, ≥0x100→0xff, <0→FUN_0047f3e0) and color(op5, <0→FUN_0047f3e0) → FUN_0047ea00. See docs/engine-re.md gfx op-contract table."
|
||||
summary = "0x202 (handle)(x)(y)(alpha)(color) — gfx cmd-type 0xb. Handler gfx_op_0x202_blit_color @0x4228d0: worker gfx_op_0x202_worker_set_color_anim @0x47ea00 sets an ANIMATED color/alpha target (obj+0x64) + anim bit; packs ARGB from alpha(op4, ≥0x100→0xff, <0→FUN_0047f3e0) and color(op5, <0→FUN_0047f3e0). C# VM (2026-07-08 blend slice): routes through GfxState.SetObjectColor → the compositor applies STATIC alpha/tint (BlendKind.Alpha); smooth color-anim interpolation deferred. See docs/engine-re.md §Blend & transparency."
|
||||
noop_headless = false
|
||||
source = "investigation"
|
||||
confidence = "high"
|
||||
@@ -4832,7 +4832,7 @@ abi_source = "kelebek+decode-validated"
|
||||
[opcode.semantics]
|
||||
name = "gfx-draw-color"
|
||||
category = "draw"
|
||||
summary = "0x203 (handle)(v)(alpha)(color) — gfx cmd-type 9. Handler gfx_op_0x203_draw_color @0x4229a0: draws object `handle` with op2 + a packed ARGB from alpha(op3)/color(op4) → FUN_0047e9b0. Sibling of 0x202 with one fewer positional arg. See docs/engine-re.md gfx op-contract table."
|
||||
summary = "0x203 (handle)(v)(alpha)(color) — gfx cmd-type 9. Handler gfx_op_0x203_draw_color @0x4229a0: worker gfx_op_0x203_worker_set_color @0x47e9b0 sets a STATIC color/alpha (obj+0x60), no anim bit; packs ARGB from alpha(op3)/color(op4). Sibling of 0x202 (one fewer positional arg). C# VM (2026-07-08 blend slice): routes through GfxState.SetObjectColor → compositor applies static alpha/tint (BlendKind.Alpha). See docs/engine-re.md §Blend & transparency."
|
||||
noop_headless = false
|
||||
source = "investigation"
|
||||
confidence = "high"
|
||||
|
||||
Reference in New Issue
Block a user