feat(render): implement Campaign AR and terrain fidelity

This commit is contained in:
Erik 2026-08-22 13:13:29 +02:00
parent 99cf26e00c
commit 7a5f96ede5
368 changed files with 50611 additions and 950 deletions

View file

@ -16,8 +16,8 @@ namespace AcDream.App.Rendering.Wb;
/// <para>Two structural differences from V4c. It records into the pass
/// <c>VulkanWorldScenePhase</c> opened rather than opening
/// <c>"envcell-shells"</c> of its own, because the frame's one backbuffer pass
/// resolves. And there is no binding-9 texture table: V4t moved the slot onto
/// the device, and on Vulkan that table is set 2, which the encoder binds.</para>
/// resolves. And the global texture table is set 2 rather than a storage
/// buffer: storage binding 9 is now #226's per-instance detail category.</para>
/// </summary>
public sealed unsafe partial class EnvCellRenderer
{
@ -27,10 +27,14 @@ public sealed unsafe partial class EnvCellRenderer
private IGpuPipeline? _opaquePipeline;
private IGpuPipeline? _alphaPipeline;
private IGpuPipeline? _additivePipeline;
private IGpuPipeline? _detailPipeline;
private IGpuPipeline? _transparentDetailPipeline;
private readonly TerrainAtlas.RetailDetailTextureBinding _environmentDetail;
private readonly Func<bool> _buildingDetailEnabled;
/// <summary>
/// The RHI arm's constructor. It also completes <c>Initialize</c>'s job: the
/// three pipelines ARE this renderer's program, so there is no second step
/// five pipelines ARE this renderer's program, so there is no second step
/// and no <c>Shader</c> to hand in.
/// </summary>
internal EnvCellRenderer(
@ -38,13 +42,17 @@ public sealed unsafe partial class EnvCellRenderer
ICurrentGpuFrameSource frames,
IWorldPassScope scope,
ObjectMeshManager meshManager,
WbFrustum frustum)
WbFrustum frustum,
TerrainAtlas.RetailDetailTextureBinding environmentDetail = default,
Func<bool>? buildingDetailEnabled = null)
{
_device = device ?? throw new ArgumentNullException(nameof(device));
_frames = frames ?? throw new ArgumentNullException(nameof(frames));
_scope = scope ?? throw new ArgumentNullException(nameof(scope));
_meshManager = meshManager ?? throw new ArgumentNullException(nameof(meshManager));
_frustum = frustum ?? throw new ArgumentNullException(nameof(frustum));
_environmentDetail = environmentDetail;
_buildingDetailEnabled = buildingDetailEnabled ?? DisableDetailTextures;
_opaquePipeline = CreateShellPipeline(
device, "envcell-opaque", GpuBlendMode.None, depthWrite: true, scope.SampleCount);
@ -52,9 +60,29 @@ public sealed unsafe partial class EnvCellRenderer
device, "envcell-alpha", GpuBlendMode.StraightAlpha, depthWrite: false, scope.SampleCount);
_additivePipeline = CreateShellPipeline(
device, "envcell-additive", GpuBlendMode.Additive, depthWrite: false, scope.SampleCount);
_detailPipeline = CreateShellPipeline(
device,
"envcell-retail-detail",
GpuBlendMode.RetailDetail,
depthWrite: true,
scope.SampleCount,
shaderName: "mesh_detail",
depthCompare: RetailDetailTextureContract.DetailDepthCompare(
transparent: false));
_transparentDetailPipeline = CreateShellPipeline(
device,
"envcell-retail-detail-alpha",
GpuBlendMode.RetailDetail,
depthWrite: false,
scope.SampleCount,
shaderName: "mesh_detail",
depthCompare: RetailDetailTextureContract.DetailDepthCompare(
transparent: true));
_initialized = true;
}
private static bool DisableDetailTextures() => false;
/// <summary>
/// One pipeline per blend state the shell pass uses. Everything else is
/// shared: <c>mesh_modern</c>, the 32-byte world-mesh vertex, triangle lists,
@ -70,15 +98,17 @@ public sealed unsafe partial class EnvCellRenderer
string name,
GpuBlendMode blend,
bool depthWrite,
int sampleCount) =>
int sampleCount,
string shaderName = "mesh_modern",
GpuCompareOp depthCompare = GpuCompareOp.Less) =>
device.CreatePipeline(new GpuPipelineDescription
{
Name = name,
Shaders = new GpuShaderSet("mesh_modern"),
Shaders = new GpuShaderSet(shaderName),
VertexLayout = GpuVertexLayout.WorldMesh,
Topology = GpuPrimitiveTopology.TriangleList,
Blend = blend,
Depth = new GpuDepthState(Test: true, Write: depthWrite, GpuCompareOp.Less),
Depth = new GpuDepthState(Test: true, Write: depthWrite, depthCompare),
Cull = GpuCullMode.Back,
FrontFace = GpuFrontFace.Clockwise,
AlphaToCoverage = false,
@ -196,6 +226,7 @@ public sealed unsafe partial class EnvCellRenderer
BindRingSection<int>(
encoder, frame, GpuBindingModel.StorageInstanceLightSets,
_lightSetData.AsSpan(0, uniqueInstanceCount * lightStride));
BindEnvironmentDetailCategory(encoder, frame);
// The frame-global sections, bound after this renderer's own binds
// because those binds are what select the descriptor scope.
@ -210,6 +241,9 @@ public sealed unsafe partial class EnvCellRenderer
MemoryMarshal.AsBytes(_commands.AsSpan(0, totalDraws)).CopyTo(commands.Data);
IGpuBuffer commandBuffer = commands.Buffer;
uint commandBase = commands.OffsetBytes;
bool detailEnabled = RetailDetailTextureContract.ShouldRender(
_buildingDetailEnabled(),
_environmentDetail);
for (int drawRangeIndex = 0; drawRangeIndex < _mdiDrawRanges.Count; drawRangeIndex++)
{
@ -222,13 +256,16 @@ public sealed unsafe partial class EnvCellRenderer
if (cullMode == CullMode.Landblock) cullMode = CullMode.None;
bool isAdditive = groupIndex >= 4;
IGpuPipeline rangeBasePipeline = isAdditive
? _additivePipeline!
: _alphaPipeline!;
if (renderPass == WbRenderPass.Transparent)
{
// Blend state is the pipeline's; switching variants mid-pass has
// to re-establish the mesh, which is vertex-array state.
BindPipelineWithMesh(
encoder,
isAdditive ? _additivePipeline! : _alphaPipeline!,
rangeBasePipeline,
mesh);
}
@ -241,12 +278,85 @@ public sealed unsafe partial class EnvCellRenderer
: (int)renderPass;
pushConstants.DrawIdOffset = drawRange.FirstCommand;
encoder.SetPushConstants(in pushConstants);
// Retail DrawMesh's two-pass fallback redraws each transparent
// RenderMeshSubset immediately, before the next delayed-alpha
// subset. Preserve that base/detail adjacency so another shell or
// particle cannot be composited between the two contributions.
if (renderPass == WbRenderPass.Transparent && detailEnabled)
{
int rangeEnd = drawRange.FirstCommand + drawRange.CommandCount;
for (int command = drawRange.FirstCommand; command < rangeEnd; command++)
{
BindPipelineWithMesh(encoder, rangeBasePipeline, mesh);
SetCullMode(encoder, cullMode);
pushConstants.RenderPass = isAdditive
? (int)renderPass | 0x100
: (int)renderPass;
pushConstants.DrawIdOffset = command;
pushConstants.TextureIndexA = 0;
pushConstants.ParamA = 0f;
pushConstants.ParamB = 0f;
encoder.SetPushConstants(in pushConstants);
encoder.MultiDrawIndexedIndirect(
commandBuffer,
commandBase + (uint)(command * sizeof(DrawElementsIndirectCommand)),
1,
(uint)sizeof(DrawElementsIndirectCommand));
BindPipelineWithMesh(encoder, _transparentDetailPipeline!, mesh);
SetCullMode(encoder, cullMode);
pushConstants.DrawIdOffset = command;
pushConstants.TextureIndexA = _environmentDetail.TextureSlot.Index;
pushConstants.ParamA = _environmentDetail.Tiling;
pushConstants.ParamB = 0f;
encoder.SetPushConstants(in pushConstants);
encoder.MultiDrawIndexedIndirect(
commandBuffer,
commandBase + (uint)(command * sizeof(DrawElementsIndirectCommand)),
1,
(uint)sizeof(DrawElementsIndirectCommand));
}
continue;
}
encoder.MultiDrawIndexedIndirect(
commandBuffer,
commandBase + (uint)(drawRange.FirstCommand * sizeof(DrawElementsIndirectCommand)),
(uint)drawRange.CommandCount,
(uint)sizeof(DrawElementsIndirectCommand));
}
// Retail DrawEnvCell category (2). Replay the already-filtered opaque
// shell commands, including ClipMap built-mesh subsets, and apply the
// 10-50 m positive-view-depth fade. The existing
// "Building Detail Textures" option gates both this and buildings,
// matching LScape::ChangeRegion.
if (renderPass == WbRenderPass.Opaque
&& detailEnabled)
{
BindPipelineWithMesh(encoder, _detailPipeline!, mesh);
pushConstants.RenderPass = 0;
pushConstants.TextureIndexA = _environmentDetail.TextureSlot.Index;
pushConstants.ParamA = _environmentDetail.Tiling;
pushConstants.ParamB = 0f;
for (int drawRangeIndex = 0; drawRangeIndex < _mdiDrawRanges.Count; drawRangeIndex++)
{
MdiDrawRange drawRange = _mdiDrawRanges[drawRangeIndex];
var cullMode = (CullMode)(drawRange.GroupIndex % 4);
if (cullMode == CullMode.Landblock)
cullMode = CullMode.None;
SetCullMode(encoder, cullMode);
pushConstants.DrawIdOffset = drawRange.FirstCommand;
encoder.SetPushConstants(in pushConstants);
encoder.MultiDrawIndexedIndirect(
commandBuffer,
commandBase + (uint)(drawRange.FirstCommand * sizeof(DrawElementsIndirectCommand)),
(uint)drawRange.CommandCount,
(uint)sizeof(DrawElementsIndirectCommand));
}
}
}
private void BindPipelineWithMesh(
@ -315,6 +425,25 @@ public sealed unsafe partial class EnvCellRenderer
(uint)byteCount);
}
/// <summary>
/// Binds one category word for <c>mesh_detail.vert</c>'s statically used
/// binding 9. EnvCell draws select their renderer-wide category via
/// <c>uParamB=0</c>, so the value is semantically unused, but Vulkan still
/// requires the declared descriptor to be valid.
/// </summary>
internal static void BindEnvironmentDetailCategory(
IGpuPassEncoder encoder,
IGpuFrame frame)
{
Span<uint> category = stackalloc uint[1];
category[0] = 1u;
BindRingSection<uint>(
encoder,
frame,
GpuBindingModel.StorageInstanceDetailCategory,
category);
}
private void DisposeRhiResources()
{
_opaquePipeline?.Dispose();
@ -323,5 +452,9 @@ public sealed unsafe partial class EnvCellRenderer
_alphaPipeline = null;
_additivePipeline?.Dispose();
_additivePipeline = null;
_detailPipeline?.Dispose();
_detailPipeline = null;
_transparentDetailPipeline?.Dispose();
_transparentDetailPipeline = null;
}
}