feat(render): implement Campaign AR and terrain fidelity
This commit is contained in:
parent
99cf26e00c
commit
7a5f96ede5
368 changed files with 50611 additions and 950 deletions
|
|
@ -28,6 +28,30 @@ namespace AcDream.App.Rendering;
|
|||
/// </summary>
|
||||
public sealed class TerrainAtlas : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Retail's category-scoped detail surface, resolved from one
|
||||
/// <c>TexMerge.TerrainDesc</c> entry. The texture-table slot samples a
|
||||
/// one-layer 2-D array so it uses the same backend-neutral table contract
|
||||
/// as every other world texture.
|
||||
/// </summary>
|
||||
internal readonly record struct RetailDetailTextureBinding(
|
||||
GpuTextureSlot TextureSlot,
|
||||
float Tiling,
|
||||
uint SurfaceTextureId,
|
||||
uint RenderSurfaceId,
|
||||
int Width,
|
||||
int Height)
|
||||
{
|
||||
public bool IsAvailable =>
|
||||
TextureSlot.IsAssigned
|
||||
&& SurfaceTextureId != 0
|
||||
&& RenderSurfaceId != 0;
|
||||
}
|
||||
|
||||
private sealed record DetailTextureResource(
|
||||
IGpuTexture Texture,
|
||||
RetailDetailTextureBinding Binding);
|
||||
|
||||
public IReadOnlyDictionary<uint, uint> TerrainTypeToLayer { get; }
|
||||
public int LayerCount { get; }
|
||||
/// <summary>
|
||||
|
|
@ -54,6 +78,18 @@ public sealed class TerrainAtlas : IDisposable
|
|||
/// <summary>RCode for each RoadMap, parallel to <see cref="RoadAlphaLayers"/>.</summary>
|
||||
public IReadOnlyList<uint> RoadAlphaRCodes { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Retail detail category 1. <c>DrawBuilding</c> is the only live object
|
||||
/// path that consumes it; ordinary scenery and terrain do not.
|
||||
/// </summary>
|
||||
internal RetailDetailTextureBinding BuildingDetailTexture { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Retail detail category 2. <c>DrawEnvCell</c> consumes it for interior
|
||||
/// cell shells.
|
||||
/// </summary>
|
||||
internal RetailDetailTextureBinding EnvironmentDetailTexture { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Campaign V slice V6i-2: both arrays are <see cref="IGpuTexture"/>s the
|
||||
/// device created, and both slots were registered at build time, so
|
||||
|
|
@ -72,6 +108,8 @@ public sealed class TerrainAtlas : IDisposable
|
|||
public IGpuTexture Alpha { get; } = alpha;
|
||||
public IGpuSampler AlphaSampler { get; } = alphaSampler;
|
||||
public IGpuSampler? TerrainSampler { get; set; }
|
||||
public IGpuTexture? BuildingDetailTexture { get; set; }
|
||||
public IGpuTexture? EnvironmentDetailTexture { get; set; }
|
||||
public GpuTextureSlot TerrainSlot { get; set; } = GpuTextureSlot.Unassigned;
|
||||
public GpuTextureSlot AlphaSlot { get; set; } = GpuTextureSlot.Unassigned;
|
||||
}
|
||||
|
|
@ -106,7 +144,9 @@ public sealed class TerrainAtlas : IDisposable
|
|||
IReadOnlyList<byte> roadLayers,
|
||||
IReadOnlyList<uint> cornerTCodes,
|
||||
IReadOnlyList<uint> sideTCodes,
|
||||
IReadOnlyList<uint> roadRCodes)
|
||||
IReadOnlyList<uint> roadRCodes,
|
||||
DetailTextureResource? buildingDetail,
|
||||
DetailTextureResource? environmentDetail)
|
||||
{
|
||||
_rhi = new RhiArrays(device, terrain, alpha, alphaSampler);
|
||||
TerrainTypeToLayer = map;
|
||||
|
|
@ -119,6 +159,10 @@ public sealed class TerrainAtlas : IDisposable
|
|||
CornerAlphaTCodes = cornerTCodes;
|
||||
SideAlphaTCodes = sideTCodes;
|
||||
RoadAlphaRCodes = roadRCodes;
|
||||
_rhi.BuildingDetailTexture = buildingDetail?.Texture;
|
||||
_rhi.EnvironmentDetailTexture = environmentDetail?.Texture;
|
||||
BuildingDetailTexture = buildingDetail?.Binding ?? default;
|
||||
EnvironmentDetailTexture = environmentDetail?.Binding ?? default;
|
||||
_rhi.AlphaSlot = device.RegisterTexture(alpha, alphaSampler);
|
||||
ApplyAnisotropic(RetailMaxAnisotropy);
|
||||
}
|
||||
|
|
@ -323,6 +367,9 @@ public sealed class TerrainAtlas : IDisposable
|
|||
int mipLevels = Wb.RhiWorldTextureArray.MipLevelsFor(maxW, maxH);
|
||||
IGpuTexture? terrainTexture = null;
|
||||
IGpuTexture? alphaTexture = null;
|
||||
IGpuSampler? detailSampler = null;
|
||||
DetailTextureResource? buildingDetail = null;
|
||||
DetailTextureResource? environmentDetail = null;
|
||||
try
|
||||
{
|
||||
terrainTexture = device.CreateTexture(new GpuTextureDescription(
|
||||
|
|
@ -378,6 +425,31 @@ public sealed class TerrainAtlas : IDisposable
|
|||
MipFilter = GpuMipFilter.None,
|
||||
});
|
||||
|
||||
// Retail LScape::SetDetailTexturing category indices:
|
||||
// 1 = building, 2 = environment/EnvCell.
|
||||
// ChangeRegion and the only reachable SmartBox caller keep
|
||||
// landscape (0) and object (3) disabled, so do not create or expose
|
||||
// those categories here.
|
||||
detailSampler = device.CreateSampler(GpuSamplerDescription.WorldRepeat);
|
||||
if (terrainDesc is { Count: > 1 })
|
||||
{
|
||||
buildingDetail = TryCreateDetailTexture(
|
||||
device,
|
||||
dats,
|
||||
detailSampler,
|
||||
terrainDesc[1],
|
||||
"building");
|
||||
}
|
||||
if (terrainDesc is { Count: > 2 })
|
||||
{
|
||||
environmentDetail = TryCreateDetailTexture(
|
||||
device,
|
||||
dats,
|
||||
detailSampler,
|
||||
terrainDesc[2],
|
||||
"environment");
|
||||
}
|
||||
|
||||
Console.WriteLine(
|
||||
$"TerrainAtlas: {layerCount} terrain layers at {maxW}x{maxH} ({mipLevels} mip levels)");
|
||||
Console.WriteLine(
|
||||
|
|
@ -399,16 +471,117 @@ public sealed class TerrainAtlas : IDisposable
|
|||
alpha.RoadLayers,
|
||||
alpha.CornerTCodes,
|
||||
alpha.SideTCodes,
|
||||
alpha.RoadRCodes);
|
||||
alpha.RoadRCodes,
|
||||
buildingDetail,
|
||||
environmentDetail);
|
||||
}
|
||||
catch
|
||||
{
|
||||
DisposeDetailTextureResource(device, environmentDetail);
|
||||
DisposeDetailTextureResource(device, buildingDetail);
|
||||
alphaTexture?.Dispose();
|
||||
terrainTexture?.Dispose();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private static DetailTextureResource? TryCreateDetailTexture(
|
||||
IGpuDevice device,
|
||||
IDatReaderWriter dats,
|
||||
IGpuSampler sampler,
|
||||
DatReaderWriter.Types.TMTerrainDesc terrain,
|
||||
string categoryName)
|
||||
{
|
||||
uint surfaceTextureId = (uint)terrain.TerrainTex.DetailTextureId;
|
||||
if (surfaceTextureId == 0)
|
||||
return null;
|
||||
|
||||
SurfaceTexture? surfaceTexture = dats.Get<SurfaceTexture>(surfaceTextureId);
|
||||
if (surfaceTexture is null || surfaceTexture.Textures.Count == 0)
|
||||
{
|
||||
Console.WriteLine(
|
||||
$"WARN: retail {categoryName} detail SurfaceTexture "
|
||||
+ $"0x{surfaceTextureId:X8} missing");
|
||||
return null;
|
||||
}
|
||||
|
||||
uint renderSurfaceId = (uint)surfaceTexture.Textures[0];
|
||||
RenderSurface? renderSurface = dats.Get<RenderSurface>(renderSurfaceId);
|
||||
if (renderSurface is null)
|
||||
{
|
||||
Console.WriteLine(
|
||||
$"WARN: retail {categoryName} detail RenderSurface "
|
||||
+ $"0x{renderSurfaceId:X8} missing");
|
||||
return null;
|
||||
}
|
||||
|
||||
Palette? palette = renderSurface.DefaultPaletteId != 0
|
||||
? dats.Get<Palette>(renderSurface.DefaultPaletteId)
|
||||
: null;
|
||||
DecodedTexture decoded = SurfaceDecoder.DecodeRenderSurface(
|
||||
renderSurface,
|
||||
palette);
|
||||
if (ReferenceEquals(decoded, DecodedTexture.Magenta))
|
||||
{
|
||||
Console.WriteLine(
|
||||
$"WARN: retail {categoryName} detail RenderSurface "
|
||||
+ $"0x{renderSurfaceId:X8} failed to decode");
|
||||
return null;
|
||||
}
|
||||
|
||||
int mipLevels = Wb.RhiWorldTextureArray.MipLevelsFor(
|
||||
decoded.Width,
|
||||
decoded.Height);
|
||||
IGpuTexture? texture = null;
|
||||
GpuTextureSlot slot = GpuTextureSlot.Unassigned;
|
||||
try
|
||||
{
|
||||
texture = device.CreateTexture(new GpuTextureDescription(
|
||||
$"retail-detail-{categoryName}",
|
||||
GpuTextureKind.Texture2DArray,
|
||||
GpuTextureFormat.Rgba8Unorm,
|
||||
decoded.Width,
|
||||
decoded.Height,
|
||||
LayerCount: 1,
|
||||
MipLevelCount: mipLevels));
|
||||
texture.Upload(0, 0, decoded.Rgba8);
|
||||
texture.GenerateMipChain();
|
||||
slot = device.RegisterTexture(texture, sampler);
|
||||
var binding = new RetailDetailTextureBinding(
|
||||
slot,
|
||||
terrain.TerrainTex.DetailTexTiling,
|
||||
surfaceTextureId,
|
||||
renderSurfaceId,
|
||||
decoded.Width,
|
||||
decoded.Height);
|
||||
Console.WriteLine(
|
||||
$"Retail detail {categoryName}: SurfaceTexture "
|
||||
+ $"0x{surfaceTextureId:X8} -> RenderSurface "
|
||||
+ $"0x{renderSurfaceId:X8}, {decoded.Width}x{decoded.Height}, "
|
||||
+ $"tiling={binding.Tiling}");
|
||||
return new DetailTextureResource(texture, binding);
|
||||
}
|
||||
catch
|
||||
{
|
||||
if (slot.IsAssigned)
|
||||
device.ReleaseTextureSlot(slot);
|
||||
texture?.Dispose();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private static void DisposeDetailTextureResource(
|
||||
IGpuDevice device,
|
||||
DetailTextureResource? resource)
|
||||
{
|
||||
if (resource is null)
|
||||
return;
|
||||
|
||||
if (resource.Binding.IsAvailable)
|
||||
device.ReleaseTextureSlot(resource.Binding.TextureSlot);
|
||||
resource.Texture.Dispose();
|
||||
}
|
||||
|
||||
private static DecodedTexture WhitePixel() =>
|
||||
new([0xFF, 0xFF, 0xFF, 0xFF], 1, 1);
|
||||
|
||||
|
|
@ -495,13 +668,20 @@ public sealed class TerrainAtlas : IDisposable
|
|||
Console.WriteLine($"TerrainAtlas: anisotropic updated to {level}x");
|
||||
}
|
||||
|
||||
private bool _disposed;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
// Slice V4t's teardown rule: the device dies with its callers, so the
|
||||
// table entries are not released here — deferring through a
|
||||
// possibly-disposed retirement queue would turn a clean shutdown into a
|
||||
// throw. The images themselves route through the device's retirement
|
||||
// queue, which is what IGpuTexture.Dispose does.
|
||||
if (_disposed)
|
||||
return;
|
||||
_disposed = true;
|
||||
|
||||
// Slice V4t's teardown rule remains load-bearing: GameWindowLifetime
|
||||
// disposes the device before this atlas, so normal teardown must not
|
||||
// call back into its texture table. Build/registration failure paths
|
||||
// above still release detail slots while the device is known alive.
|
||||
_rhi.EnvironmentDetailTexture?.Dispose();
|
||||
_rhi.BuildingDetailTexture?.Dispose();
|
||||
_rhi.Alpha.Dispose();
|
||||
_rhi.Terrain.Dispose();
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue