sky(phase-8): retail-faithful night sky + README refresh

Iteration on the sky rendering pipeline to restore stars/moon visibility
at night and fix washed-out grey daytime clouds. Key fixes:

* sky.frag: disable fog-mix on sky meshes. Retail's keyframe FogEnd
  (0..400m at midnight, up to 2400m during day) is calibrated for
  terrain; sky meshes are authored at radii 1050-14271m which sits
  past FogEnd universally, causing every sky pixel to saturate to
  fogColor (dark navy). Stars, moon, dome texture all got
  obliterated. The horizon-glow trade-off is noted in the shader
  comment; research item to find retail's sky-specific fog range
  later.

* SkyRenderer + sky.frag: promote rep.Luminosity into uEmissive so the
  vertex lighting saturates properly for bright keyframes. Retail's
  FUN_0059da60 non-luminous path writes rep.Luminosity into
  material.Emissive via the cache +0x3c slot; we were instead using
  it as a post-fragment multiply which could only dim, never brighten.
  Net effect: daytime clouds now render saturated white, dome dims
  correctly at night (rep.Luminosity=0.11 → Emissive=0.11), stars
  and moon unchanged.

* terrain.vert: MIN_FACTOR 0.08 -> 0.0 per retail FUN_00532440 decompile
  (DAT_00796344 ambient-floor = 0.0). Back-lit terrain now falls to
  pure ambient rather than getting an 8% sun floor.

New research / tooling (no runtime impact):

* docs/research/2026-04-24-lambert-brightness-split.md — retail's
  ambient-brightness formula pinned from PE .rdata read + live
  RetailTimeProbe capture: effAmbBright = AmbBright + |sunDir| * 0.2
  where scale constant 0x0079a1e8 = 0.2f exactly.

* docs/research/2026-04-23-lightning-real.md — research note on the
  dat-baked PhysicsScript-driven lightning path (Rainy DayGroup has
  explicit PES-triggered flash SkyObjects with 5ms time windows).

* Corrections stapled to sky-decompile-hunt-{B,C}.md: DAT_00842778 is
  DirColor, DAT_0084277c is AmbColor (the hunt docs had the swap
  backwards).

* tools/RetailTimeProbe/Program.cs: extended with pid=NNNN selector,
  sky global probe (DirColor/AmbColor/AmbBright/sunDir/cache.amb),
  and the 0x0079a1e8 scale-factor readout.

* tools/SkyObjectInspect/: throwaway dat-inspector built by the Opus
  deep-dive agent. Identified GfxObj 0x010015EF as the stars layer
  (A8R8G8B8 128x128 texture, 4% bright-pixel ratio).

* src/AcDream.App/Rendering/TextureCache.cs: per-texture alpha
  histogram dump under ACDREAM_DUMP_SKY=1 for diagnosing "are the
  clouds decoded with proper alpha" type questions.

README: rewrite to reflect current state (playable pre-alpha rendering
Dereth with animated characters, day-night cycle, weather, etc.)
instead of the stale "Phase 0 dat inventory only" description.

All 742 tests green.
This commit is contained in:
Erik 2026-04-24 20:34:36 +02:00
parent 889b235886
commit 1d54880213
12 changed files with 1217 additions and 43 deletions

View file

@ -1,6 +1,6 @@
// RetailTimeProbe — read the live retail acclient.exe process memory and
// dump its TimeOfDay struct so we can compare against acdream's computed
// calendar values.
// dump its TimeOfDay struct + sky-lighting global block so we can compare
// against acdream's computed calendar / SkyKeyframe values.
//
// Decompile provenance (docs/research/2026-04-23-sky-decompile-hunt-C.md
// §4 and the daygroup-selection research):
@ -18,6 +18,30 @@
// TimeOfDay +0x68 int — DayOfYear
// TimeOfDay +0x6C int — SeasonIndex
//
// Sky-lighting globals (hunt-C §1, with 2026-04-24 label correction — the
// DirColor/AmbColor labeling in §1/§2/§5 was backwards; we use the
// corrected mapping):
//
// DAT_00842778 4 ARGB DirColor (directional / sun color)
// DAT_0084277c 4 ARGB AmbColor (ambient color)
// DAT_00842780 4 float AmbBright (ambient brightness scalar, also fog-start offset)
// DAT_00842784 4 ARGB FogSecondary
// DAT_00842788 4 ARGB FogPrimary
// DAT_00842950 12 3×flt sunDir XYZ (|v| = DirBright, NOT a unit vector)
// DAT_0084295c 4 float DirBright floor (MinWorldFog clamp)
// DAT_0079a1e8 4 float fog-distance scale factor (used in
// fogDist = |sunDir| * _DAT_0079a1e8 + AmbBright)
//
// Cached D3D light struct (written by FUN_00505f30:6058-6065 and
// FUN_004530e0:2083-2086 — see chunk_00500000.c / chunk_00450000.c):
//
// DAT_008682b0 12 3×flt light.Ambient pre-mul = fogTint * AmbBright
// (set inside FUN_004530e0 via FUN_00451a60(DirColor))
// DAT_008682bc 12 3×flt sunDir copy (fVar1/2/3 = X/Y/Z)
// DAT_008682c8 12 3×flt sunDir primary
// DAT_008682d4 4 uint reserved (written 0)
// DAT_008682d8 4 uint light type (3 = directional)
//
// The acclient.exe referenced in the decompile has preferred image base
// 0x00400000 (standard Win32 default). If ASLR is enabled the actual
// load address will differ — we compute relative to Process.MainModule
@ -48,6 +72,27 @@ internal static class Program
private const int Off_DayOfYear = 0x68; // int
private const int Off_SeasonIndex = 0x6C; // int
// Sky-lighting globals (static VAs in acclient.exe image).
private const uint SkyBlockBase = 0x00842778u; // DirColor / start of sky block
private const uint SkyBlockSize = 72u; // 0x00842778..0x008427c0 = 72 bytes
private const uint DAT_DirColor = 0x00842778u; // ARGB
private const uint DAT_AmbColor = 0x0084277cu; // ARGB
private const uint DAT_AmbBright = 0x00842780u; // float
private const uint DAT_FogSecondary = 0x00842784u; // ARGB
private const uint DAT_FogPrimary = 0x00842788u; // ARGB
private const uint DAT_SunDirX = 0x00842950u; // float
private const uint DAT_SunDirY = 0x00842954u; // float
private const uint DAT_SunDirZ = 0x00842958u; // float
private const uint DAT_DirBrightMin = 0x0084295cu; // float (MinWorldFog / DirBright floor)
private const uint DAT_FogScale = 0x0079a1e8u; // float (|sun|·scale factor)
// Cached D3D light struct.
private const uint DAT_LightAmbient = 0x008682b0u; // 3×float (light.Ambient pre-mul)
private const uint DAT_LightDirCopy = 0x008682bcu; // 3×float (sunDir copy)
private const uint DAT_LightDirMain = 0x008682c8u; // 3×float (sunDir primary)
private const uint DAT_LightReserved = 0x008682d4u; // uint
private const uint DAT_LightType = 0x008682d8u; // uint (3 = directional)
// Process access rights needed: read memory + query info.
private const uint PROCESS_VM_READ = 0x0010u;
private const uint PROCESS_QUERY_INFORMATION = 0x0400u;
@ -55,22 +100,51 @@ internal static class Program
private static int Main(string[] args)
{
// Retail's process name is "acclient" (.exe stripped by Process API).
// Allow override from the command line just in case.
string processName = args.Length > 0 ? args[0] : "acclient";
Console.WriteLine($"RetailTimeProbe — scanning for process \"{processName}\"...");
Process[] procs = Process.GetProcessesByName(processName);
if (procs.Length == 0)
// args[0] = process name OR "pid=NNNN" to target a specific pid.
string processName = "acclient";
int? requestedPid = null;
foreach (var a in args)
{
Console.Error.WriteLine(
$"no process named \"{processName}\" is running. Launch the retail AC client " +
"and log in to a character first, then re-run this probe.");
return 2;
if (a.StartsWith("pid=", StringComparison.OrdinalIgnoreCase) &&
int.TryParse(a.Substring(4), out var pidParsed))
requestedPid = pidParsed;
else
processName = a;
}
if (procs.Length > 1)
Console.WriteLine($"(found {procs.Length} matching processes — probing the first)");
Process target = procs[0];
Process target;
if (requestedPid is int pid)
{
try { target = Process.GetProcessById(pid); }
catch (Exception ex)
{
Console.Error.WriteLine($"no process with pid={pid}: {ex.Message}");
return 2;
}
Console.WriteLine($"RetailTimeProbe — targeting pid={pid} ({target.ProcessName})");
}
else
{
Console.WriteLine($"RetailTimeProbe — scanning for process \"{processName}\"...");
Process[] procs = Process.GetProcessesByName(processName);
if (procs.Length == 0)
{
Console.Error.WriteLine(
$"no process named \"{processName}\" is running. Launch the retail AC client " +
"and log in to a character first, then re-run this probe.");
return 2;
}
if (procs.Length > 1)
{
Console.WriteLine($"(found {procs.Length} matching processes — use `pid=NNNN` to target a specific one)");
foreach (var p in procs)
{
Console.WriteLine($" pid={p.Id} start={p.StartTime:HH:mm:ss} title=\"{p.MainWindowTitle}\"");
}
Console.WriteLine("(probing the first)");
}
target = procs[0];
}
Console.WriteLine(
$"pid={target.Id} name={target.ProcessName} start={target.StartTime:HH:mm:ss} " +
$"mainmodule={target.MainModule?.FileName ?? "<null>"}");
@ -155,6 +229,9 @@ internal static class Program
double inferredTick = curDayStart + dayFraction * (curDayEnd - curDayStart);
Console.WriteLine($" inferred retail tick = {inferredTick:F3}");
Console.WriteLine($" retail LCG seed = year*secsPerDay + dayOfYear = {year}*{secsPerDayI}+{dayOfYear} = {(long)year * secsPerDayI + dayOfYear}");
// ---------------- Sky-lighting block dump ----------------
DumpSkyBlock(handle, moduleBase);
return 0;
}
finally
@ -163,6 +240,103 @@ internal static class Program
}
}
private static void DumpSkyBlock(IntPtr handle, IntPtr moduleBase)
{
// Helper to relocate a preferred-image-base VA onto the live module.
IntPtr Reloc(uint va) =>
(IntPtr)(moduleBase.ToInt64() + (long)(va - PreferredImageBase));
Console.WriteLine();
Console.WriteLine("=========== Sky globals (retail acclient.exe, live) ===========");
// Raw block dump for the contiguous 72-byte region at 0x00842778.
byte[] block = ReadBytes(handle, Reloc(SkyBlockBase), (int)SkyBlockSize);
Console.Write($" [raw {SkyBlockBase:X8}..{SkyBlockBase + SkyBlockSize - 1:X8}]");
for (int i = 0; i < block.Length; i++)
{
if ((i % 16) == 0) Console.Write($"\n +{i:X2}:");
Console.Write($" {block[i]:X2}");
}
Console.WriteLine();
Console.WriteLine();
// Primary field-by-field decode.
uint dirColor = ReadUInt32(handle, Reloc(DAT_DirColor));
uint ambColor = ReadUInt32(handle, Reloc(DAT_AmbColor));
float ambBright = ReadSingle(handle, Reloc(DAT_AmbBright));
uint fogSecondary = ReadUInt32(handle, Reloc(DAT_FogSecondary));
uint fogPrimary = ReadUInt32(handle, Reloc(DAT_FogPrimary));
float sunX = ReadSingle(handle, Reloc(DAT_SunDirX));
float sunY = ReadSingle(handle, Reloc(DAT_SunDirY));
float sunZ = ReadSingle(handle, Reloc(DAT_SunDirZ));
float dirBrightMin = ReadSingle(handle, Reloc(DAT_DirBrightMin));
float fogScale = ReadSingle(handle, Reloc(DAT_FogScale));
double dirBright = Math.Sqrt((double)sunX * sunX + (double)sunY * sunY + (double)sunZ * sunZ);
Console.WriteLine($" [retail sky] DirColor = {FormatArgb(dirColor)}");
Console.WriteLine($" [retail sky] AmbColor = {FormatArgb(ambColor)}");
Console.WriteLine($" [retail sky] AmbBright = {ambBright:F4} (@0x{DAT_AmbBright:X8})");
Console.WriteLine($" [retail sky] FogPrimary = {FormatArgb(fogPrimary)} (@0x{DAT_FogPrimary:X8})");
Console.WriteLine($" [retail sky] FogSecondary = {FormatArgb(fogSecondary)} (@0x{DAT_FogSecondary:X8})");
Console.WriteLine($" [retail sky] sunDir = ({sunX,7:F4},{sunY,7:F4},{sunZ,7:F4}) |dir|=DirBright={dirBright:F4}");
Console.WriteLine($" [retail sky] DirBrightMin = {dirBrightMin:F4} (@0x{DAT_DirBrightMin:X8}, MinWorldFog clamp)");
Console.WriteLine($" [retail sky] 0x0079a1e8 = {fogScale:F6} (fog |sun|-scale factor)");
// Derived fog distance (matches FUN_00505f30:6067-6069):
// fogDist = |sunDir| * _DAT_0079a1e8 + AmbBright
double fogDist = dirBright * fogScale + ambBright;
Console.WriteLine($" [retail sky] derived fogDist = |sun|*scale + AmbBright = {fogDist:F4}");
// ---- Cached D3D light struct at 0x008682b0..0x008682d8 (40 bytes) ----
Console.WriteLine();
Console.WriteLine(" -- cached D3D light struct (0x008682b0..0x008682d8) --");
float ambR = ReadSingle(handle, Reloc(DAT_LightAmbient + 0));
float ambG = ReadSingle(handle, Reloc(DAT_LightAmbient + 4));
float ambB = ReadSingle(handle, Reloc(DAT_LightAmbient + 8));
float dcX = ReadSingle(handle, Reloc(DAT_LightDirCopy + 0));
float dcY = ReadSingle(handle, Reloc(DAT_LightDirCopy + 4));
float dcZ = ReadSingle(handle, Reloc(DAT_LightDirCopy + 8));
float dmX = ReadSingle(handle, Reloc(DAT_LightDirMain + 0));
float dmY = ReadSingle(handle, Reloc(DAT_LightDirMain + 4));
float dmZ = ReadSingle(handle, Reloc(DAT_LightDirMain + 8));
uint reservedVal = ReadUInt32(handle, Reloc(DAT_LightReserved));
uint lightType = ReadUInt32(handle, Reloc(DAT_LightType));
Console.WriteLine($" [retail sky] cache.amb = ({ambR,7:F4},{ambG,7:F4},{ambB,7:F4}) (fogTint * AmbBright, effective light.Ambient)");
Console.WriteLine($" [retail sky] cache.dirCpy = ({dcX,7:F4},{dcY,7:F4},{dcZ,7:F4}) (008682bc/c0/c4, sunDir duplicate)");
Console.WriteLine($" [retail sky] cache.dirMain= ({dmX,7:F4},{dmY,7:F4},{dmZ,7:F4}) (008682c8/cc/d0, sunDir primary)");
Console.WriteLine($" [retail sky] cache.reserv = 0x{reservedVal:X8} (008682d4, written 0 by 00505f30:6065)");
Console.WriteLine($" [retail sky] cache.type = 0x{lightType:X8} (008682d8, 3 = directional)");
Console.WriteLine("=================================================================");
}
/// <summary>
/// Format a packed ARGB u32 as "#AARRGGBB (r=.. g=.. b=..)". Retail uses the
/// standard Windows D3DCOLOR layout verified against FUN_00451a60 (chunk
/// _00450000.c:615-622): float R = (u &gt;&gt; 16) &amp; 0xff, G = (u &gt;&gt; 8) &amp; 0xff,
/// B = u &amp; 0xff, each divided by 255.
/// </summary>
private static string FormatArgb(uint argb)
{
byte a = (byte)((argb >> 24) & 0xff);
byte r = (byte)((argb >> 16) & 0xff);
byte g = (byte)((argb >> 8) & 0xff);
byte b = (byte)( argb & 0xff);
return $"#{a:X2}{r:X2}{g:X2}{b:X2} (r={r / 255.0f:F3} g={g / 255.0f:F3} b={b / 255.0f:F3})";
}
private static byte[] ReadBytes(IntPtr handle, IntPtr address, int count)
{
byte[] buf = new byte[count];
if (!ReadProcessMemory(handle, address, buf, buf.Length, out _))
throw new InvalidOperationException(
$"ReadProcessMemory(0x{address.ToInt64():X8}, {count}) failed " +
$"(Win32 error {Marshal.GetLastPInvokeError()})");
return buf;
}
private static uint ReadUInt32(IntPtr handle, IntPtr address)
{
byte[] buf = new byte[4];

View file

@ -0,0 +1,175 @@
// SkyObjectInspect — throwaway probe for the Dereth stars mystery.
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using DatReaderWriter;
using DatReaderWriter.DBObjs;
using DatReaderWriter.Enums;
using DatReaderWriter.Options;
using DatReaderWriter.Types;
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);
if (!dats.TryGet<Region>(0x13000000u, out var region) || region is null)
{
Console.Error.WriteLine("ERROR: Cannot read Region 0x13000000");
return 1;
}
Console.WriteLine($"Region loaded. SkyInfo.DayGroups count: {region.SkyInfo?.DayGroups?.Count ?? -1}");
var interesting = new[] { 0, 8 };
foreach (int dg in interesting)
{
if (region.SkyInfo?.DayGroups is null || dg >= region.SkyInfo.DayGroups.Count) continue;
var group = region.SkyInfo.DayGroups[dg];
Console.WriteLine();
Console.WriteLine($"=== DayGroup[{dg}] Name=\"{group.DayName?.Value}\" Chance={group.ChanceOfOccur:F3} SkyObjs={group.SkyObjects.Count} SkyTimes={group.SkyTime.Count} ===");
for (int oi = 0; oi < group.SkyObjects.Count; oi++)
{
var so = group.SkyObjects[oi];
Console.WriteLine($" OI={oi}: Begin={so.BeginTime:F3} End={so.EndTime:F3} BeginAng={so.BeginAngle:F1} EndAng={so.EndAngle:F1} TexVel=({so.TexVelocityX:F3},{so.TexVelocityY:F3}) Gfx=0x{(uint)so.DefaultGfxObjectId:X8} Pes=0x{(uint)so.DefaultPesObjectId:X8} Props=0x{so.Properties:X8}");
}
// Show every SkyTime's SkyObjectReplace entries — this tells us if any OI
// actually changes at night.
foreach (var skytime in group.SkyTime.OrderBy(s => s.Begin))
{
Console.WriteLine($" [SkyTime @ Begin={skytime.Begin:F3}] Replaces={skytime.SkyObjReplace.Count}");
foreach (var r in skytime.SkyObjReplace)
{
Console.WriteLine($" OI={r.ObjectIndex}: Gfx=0x{(uint)r.GfxObjId:X8} Rot={r.Rotate:F2} Transp={r.Transparent:F3} Lum={r.Luminosity:F3} MaxB={r.MaxBright:F3}");
}
}
}
// Also scan ALL DayGroups for any SkyObject with BeginTime > EndTime (wrap)
// OR BeginTime in late night (>0.75) with a gfx that could be stars.
Console.WriteLine();
Console.WriteLine("=== Scan: any SkyObject with night-spanning window (begin>0.7 or end<0.3 wrap-candidate) across ALL DayGroups ===");
int nFound = 0;
if (region.SkyInfo?.DayGroups is not null)
{
for (int dg = 0; dg < region.SkyInfo.DayGroups.Count; dg++)
{
var group = region.SkyInfo.DayGroups[dg];
for (int oi = 0; oi < group.SkyObjects.Count; oi++)
{
var so = group.SkyObjects[oi];
bool wrap = so.BeginTime > so.EndTime && so.BeginTime != so.EndTime;
bool late = so.BeginTime > 0.7f;
bool early = so.EndTime < 0.3f && so.EndTime > 0f;
if (wrap || late || early)
{
Console.WriteLine($" DG[{dg}]=\"{group.DayName?.Value}\" OI={oi} Begin={so.BeginTime:F3} End={so.EndTime:F3} Gfx=0x{(uint)so.DefaultGfxObjectId:X8} wrap={wrap} late={late} early={early}");
nFound++;
}
}
}
}
Console.WriteLine($" (found {nFound} night-window candidates)");
// Candidate GfxObjs for Sunny.
var candidateIds = new uint[] { 0x010015EEu, 0x010015EFu, 0x01001F6Au, 0x01004C36u, 0x02000714u };
foreach (uint gid in candidateIds)
{
Console.WriteLine();
Console.WriteLine($"=== GfxObj 0x{gid:X8} ===");
if (gid >= 0x02000000u)
{
if (dats.TryGet<Setup>(gid, out var setup) && setup is not null)
{
Console.WriteLine($" [Setup] Parts={setup.Parts.Count}");
for (int pi = 0; pi < setup.Parts.Count; pi++)
{
uint partGid = (uint)setup.Parts[pi];
Console.WriteLine($" Part[{pi}] = GfxObj 0x{partGid:X8}");
DumpGfxObj(dats, partGid, indent: " ");
}
}
else
{
Console.WriteLine(" (not a Setup or not found)");
}
continue;
}
DumpGfxObj(dats, gid, indent: " ");
}
return 0;
static void DumpGfxObj(DatCollection dats, uint gid, string indent)
{
if (!dats.TryGet<GfxObj>(gid, out var go) || go is null)
{
Console.WriteLine($"{indent}(GfxObj 0x{gid:X8} not found)");
return;
}
Console.WriteLine($"{indent}GfxObj 0x{gid:X8}: Flags=0x{(uint)go.Flags:X8} Surfaces={go.Surfaces.Count} Polys={go.Polygons.Count} Verts={go.VertexArray?.Vertices?.Count ?? 0}");
for (int si = 0; si < go.Surfaces.Count; si++)
{
uint sid = (uint)go.Surfaces[si];
if (!dats.TryGet<Surface>(sid, out var surf) || surf is null)
{
Console.WriteLine($"{indent} Surf[{si}]=0x{sid:X8} (not found)");
continue;
}
string texDesc = DescribeTexture(dats, surf);
Console.WriteLine($"{indent} Surf[{si}]=0x{sid:X8} Type={surf.Type} Translucency={surf.Translucency:F3} Luminosity={surf.Luminosity:F3} Diffuse={surf.Diffuse:F3} Tex=[{texDesc}]");
}
}
static string DescribeTexture(DatCollection dats, Surface surf)
{
if (!(surf.Type.HasFlag(SurfaceType.Base1Image) || surf.Type.HasFlag(SurfaceType.Base1ClipMap)))
return $"solid color A=0x{surf.ColorValue.Alpha:X2} R=0x{surf.ColorValue.Red:X2} G=0x{surf.ColorValue.Green:X2} B=0x{surf.ColorValue.Blue:X2}";
uint stid = (uint)surf.OrigTextureId;
if (stid == 0) return "no-texture";
if (!dats.TryGet<SurfaceTexture>(stid, out var st) || st is null)
return $"SurfaceTex 0x{stid:X8} missing";
if (st.Textures.Count == 0) return $"SurfaceTex 0x{stid:X8} empty";
uint rsid = (uint)st.Textures[0];
if (!dats.TryGet<RenderSurface>(rsid, out var rs) || rs is null)
return $"RenderSurf 0x{rsid:X8} missing";
double brightRatio = ApproxBrightRatio(rs);
return $"{rs.Width}x{rs.Height} {rs.Format} data={rs.SourceData.Length}B palette=0x{rs.DefaultPaletteId:X8} brightRatio~{brightRatio:F3}";
}
static double ApproxBrightRatio(RenderSurface rs)
{
if (rs.SourceData is null || rs.SourceData.Length == 0) return 0;
if (rs.Format == PixelFormat.PFID_A8R8G8B8)
{
int bright = 0, total = rs.SourceData.Length / 4;
for (int i = 0; i + 4 <= rs.SourceData.Length; i += 4)
{
byte a = rs.SourceData[i];
byte r = rs.SourceData[i + 1];
byte g = rs.SourceData[i + 2];
byte b = rs.SourceData[i + 3];
if (a > 0 && (r + g + b) / 3 > 48) bright++;
}
return total > 0 ? (double)bright / total : 0;
}
if (rs.Format == PixelFormat.PFID_R8G8B8)
{
int bright = 0, total = rs.SourceData.Length / 3;
for (int i = 0; i + 3 <= rs.SourceData.Length; i += 3)
{
byte r = rs.SourceData[i];
byte g = rs.SourceData[i + 1];
byte b = rs.SourceData[i + 2];
if ((r + g + b) / 3 > 48) bright++;
}
return total > 0 ? (double)bright / total : 0;
}
int nonZero = 0;
for (int i = 0; i < rs.SourceData.Length; i++) if (rs.SourceData[i] != 0) nonZero++;
return (double)nonZero / rs.SourceData.Length;
}

View file

@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<RootNamespace>SkyObjectInspect</RootNamespace>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\references\DatReaderWriter\DatReaderWriter\DatReaderWriter.csproj" />
</ItemGroup>
</Project>