acdream/tools/RainMeshProbe/Program.cs
Erik b8e0857b87 tools(probe): add RainMeshProbe — dumps rain mesh surface + polygons + build counts
Sibling of StarsProbe/WeatherEnumerator. Targets GfxObjs 0x01004C42 and
0x01004C44 (the two rain cylinders). For each: dumps the Surface raw
record (Type bits, Translucency, Luminosity, Diffuse, ColorValue,
OrigTextureId), every polygon's SidesType + Stippling + hasPos/hasNeg
emission flags (mirroring GfxObjMesh.Build's neg-side rule), and the
final GfxObjMesh.Build() submesh+index counts.

Built per independent code-review §5: "Run one targeted probe... if one
cylinder has more than 48 indices per side-equivalent, fix the
duplicate-side/cull behavior together with the surface-opacity uniform."

Probe results (rain_mesh_probe.log, not committed):
  Surface 0x080000C5: Type=0x10112 (Base1Image|Translucent|Alpha|Additive),
    Translucency=0.5000, Luminosity=0.1484, OrigTextureId=0x050016A6.
  Polygons: all 8 are Stippling=Positive, SidesType=None, hasNeg=False.
  Build output: 1 submesh, 24 verts, 48 indices = 8 walls × 2 tris × 3.
  → SINGLE-SIDED (the duplicate-side hypothesis is disconfirmed).

Confirmed: the rim brightness excess is purely from Translucency not
being plumbed (acdream draws rain at full alpha=1.0 instead of retail's
0.5). Bonus finding: surface.Luminosity=0.1484 is also ignored by the
renderer's `effEmissive = (luminosity > 0) ? luminosity : sub.SurfLuminosity`
fallback (the local `luminosity` defaults to 1.0 so the fallback never
fires) — but that's keyed on the LUMINOUS flag bit (0x40), which the rain
surface does NOT have. Filed as follow-up.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 08:50:02 +02:00

157 lines
6.8 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// RainMeshProbe — independent code-review recommended probe (Bug A, post-#26).
//
// Per Report 1's §5: "Run one targeted probe for 0x01004C42/0x01004C44: print
// surface raw type/translucency, each polygon's SidesType/Stippling, and
// GfxObjMesh.Build() submesh/index counts. If one cylinder has more than 48
// indices per side-equivalent, fix the duplicate-side/cull behavior together
// with the surface-opacity uniform."
//
// The cylinder has 8 wall quads. With fan-triangulation each quad → 2 tris →
// 6 indices, total 48 indices per side. If pos-only emission: 48. If pos+neg:
// 96. The threshold tells us whether double-sided drawing is happening.
using System;
using System.IO;
using System.Linq;
using DatReaderWriter;
using DatReaderWriter.DBObjs;
using DatReaderWriter.Enums;
using DatReaderWriter.Options;
using DatReaderWriter.Types;
using AcDream.Core.Meshing;
using SysEnv = System.Environment;
string datDir = SysEnv.GetEnvironmentVariable("ACDREAM_DAT_DIR")
?? Path.Combine(SysEnv.GetFolderPath(SysEnv.SpecialFolder.UserProfile),
"Documents", "Asheron's Call");
Console.WriteLine($"datDir = {datDir}");
using var dats = new DatCollection(datDir, DatAccessType.Read);
uint[] gfxIds = { 0x01004C42u, 0x01004C44u };
foreach (uint gid in gfxIds) ProbeRain(dats, gid);
return 0;
static void ProbeRain(DatCollection dats, uint gid)
{
Console.WriteLine();
Console.WriteLine($"================ GfxObj 0x{gid:X8} ================");
if (!dats.TryGet<GfxObj>(gid, out var go) || go is null)
{
Console.WriteLine(" (NOT FOUND)");
return;
}
Console.WriteLine($" Flags={go.Flags}");
Console.WriteLine($" VertexArray.Vertices.Count={go.VertexArray?.Vertices.Count ?? 0}");
Console.WriteLine($" Polygons.Count={go.Polygons?.Count ?? 0}");
Console.WriteLine($" Surfaces.Count={go.Surfaces?.Count ?? 0}");
Console.WriteLine($" PhysicsPolygons.Count={go.PhysicsPolygons?.Count ?? 0}");
Console.WriteLine($" SortCenter=({go.SortCenter.X:F2},{go.SortCenter.Y:F2},{go.SortCenter.Z:F2})");
// ----- Per-Surface dump -----
Console.WriteLine();
Console.WriteLine(" --- Surfaces (raw dat record) ---");
if (go.Surfaces is { Count: > 0 })
{
for (int i = 0; i < go.Surfaces.Count; i++)
{
uint sid = (uint)go.Surfaces[i];
Console.WriteLine($" Surface[{i}] = 0x{sid:X8}");
if (!dats.TryGet<Surface>(sid, out var surf) || surf is null)
{
Console.WriteLine(" (Surface NOT FOUND)");
continue;
}
uint typeRaw = (uint)surf.Type;
Console.WriteLine($" Type=0x{typeRaw:X8} ({surf.Type})");
Console.WriteLine($" decoded bits:");
DumpFlagBits(typeRaw);
Console.WriteLine($" Translucency={surf.Translucency:F4} (1.0 - x = opacity = {1f - surf.Translucency:F4})");
Console.WriteLine($" Luminosity={surf.Luminosity:F4}");
Console.WriteLine($" Diffuse={surf.Diffuse:F4}");
Console.WriteLine($" ColorValue=" + (surf.ColorValue is null ? "null" :
$"A:{surf.ColorValue.Alpha} R:{surf.ColorValue.Red} G:{surf.ColorValue.Green} B:{surf.ColorValue.Blue}"));
Console.WriteLine($" OrigTextureId=0x{(uint)surf.OrigTextureId:X8}");
Console.WriteLine($" OrigPaletteId=0x{(uint)surf.OrigPaletteId:X8}");
}
}
// ----- Per-Polygon dump -----
Console.WriteLine();
Console.WriteLine(" --- Polygons (sides + stippling — checks Report 1 hypothesis) ---");
if (go.Polygons is { Count: > 0 })
{
int posCount = 0, negCount = 0;
foreach (var kv in go.Polygons)
{
var p = kv.Value;
// Mirror the GfxObjMesh.Build() emission rule (lines 71-91):
bool hasPos = !p.Stippling.HasFlag(StipplingType.NoPos);
bool hasNeg =
p.Stippling.HasFlag(StipplingType.Negative) ||
p.Stippling.HasFlag(StipplingType.Both) ||
(!p.Stippling.HasFlag(StipplingType.NoNeg) && p.SidesType == CullMode.Clockwise);
if (hasPos) posCount++;
if (hasNeg) negCount++;
Console.WriteLine(
$" Poly[{kv.Key,3}] VertexIds={p.VertexIds.Count} " +
$"PosSurface={p.PosSurface} NegSurface={p.NegSurface} " +
$"Stippling={p.Stippling} SidesType={p.SidesType} " +
$"hasPos={hasPos} hasNeg={hasNeg} " +
$"PosUVIdx={p.PosUVIndices.Count} NegUVIdx={p.NegUVIndices.Count}");
}
Console.WriteLine($" Build emission summary: pos-side polys={posCount} neg-side polys={negCount}");
}
// ----- GfxObjMesh.Build() output -----
Console.WriteLine();
Console.WriteLine(" --- GfxObjMesh.Build() output ---");
var subs = GfxObjMesh.Build(go, dats);
Console.WriteLine($" Submesh count: {subs.Count}");
int totalVerts = 0, totalIndices = 0;
for (int i = 0; i < subs.Count; i++)
{
var s = subs[i];
totalVerts += s.Vertices.Length;
totalIndices += s.Indices.Length;
Console.WriteLine(
$" Submesh[{i}] SurfaceId=0x{s.SurfaceId:X8} " +
$"Vertices={s.Vertices.Length} Indices={s.Indices.Length} " +
$"Translucency={s.Translucency} Luminosity={s.Luminosity:F2} " +
$"NeedsUvRepeat={s.NeedsUvRepeat}");
}
Console.WriteLine($" TOTAL: verts={totalVerts} indices={totalIndices}");
Console.WriteLine();
Console.WriteLine($" Report 1 threshold check: with 8 wall quads × 2 tris × 3 indices = 48 indices per side.");
Console.WriteLine($" pos-only emission expects ~48 indices total.");
Console.WriteLine($" pos+neg emission expects ~96 indices total.");
Console.WriteLine($" OBSERVED: {totalIndices} indices → " +
(totalIndices > 60 ? "*** DOUBLE-SIDED — duplicate-side rendering active ***" : "single-sided"));
}
static void DumpFlagBits(uint type)
{
// From docs/research/named-retail/acclient.h:5820-5836.
// Print every named SurfaceType bit that's set.
var bits = new (uint mask, string name)[]
{
(0x00000001u, "Base1Solid"),
(0x00000002u, "Base1Image"),
(0x00000004u, "Base1ClipMap"),
(0x00000010u, "Translucent"),
(0x00000020u, "Diffuse"),
(0x00000040u, "Luminous"),
(0x00000100u, "Alpha"),
(0x00000200u, "InvAlpha"),
(0x00010000u, "Additive"),
(0x00020000u, "Detail"),
(0x10000000u, "Gouraud"),
(0x40000000u, "Stippled"),
(0x80000000u, "Perspective"),
};
foreach (var (mask, name) in bits)
{
if ((type & mask) != 0)
Console.WriteLine($" {name} (0x{mask:X8})");
}
}