Opus dual-lens review of05970306+388457a7(APPROVE WITH FIXES). Four items, all landed: 1. FOG (behavioural). Retail's D3D fixed-function fog stage runs AFTER the texture-stage pipeline, so the detail contribution must be fogged, not just the base. mesh_modern.frag already fogs the base colour (applyFog(rgb, vWorldPos)) before mesh_detail's replay draws over it; mesh_detail.frag previously emitted raw detail.rgb, understating fog by f*a*(fog-detail). Fix: mesh_detail.vert now outputs vWorldPos (mirroring mesh_modern.vert); mesh_detail.frag declares the identical SceneLighting UBO and applyFog function (copied verbatim, same binding/std140/math) and fogs detail.rgb before emitting it. This collapses algebraically to retail's fog-after-combine order: (1-a)*mix(base,fog,f) + a*mix(detail,fog,f) = mix(lerp(base,detail,a),fog,f) RetailDetailTextureContract gains ExpectedFogged(base,detail,opacity,fog, fogFactor); RetailDetailTextureContractTests pins the identity across 200 random samples within 1e-6. 2. EnvCellRenderer.Rhi.cs's DrawEnvCell-category comment still said "apply the 10-50 m positive-view-depth fade" — a stale claim from before VM1 removed the fade. Replaced with the mip-chain attenuation statement that mesh_detail.vert's header comment already carries. 3. Added the test the VM1 contract required but never had: TerrainAtlas .TryCreateDetailTexture uploads a full mip chain (MipLevelCount == RhiWorldTextureArray.MipLevelsFor(w,h), GenerateMipChain called) and registers with the repeat/linear world sampler, not single-level or clamped. Drives the private method directly (reflection) against a synthetic PFID_A8R8G8B8 RenderSurface through a minimal in-memory IDatReaderWriter fake, so the lane stays hermetic (no installed DAT). 4. #226 pseudocode note: noted that retail's stage-1 OUTPUT alpha (MODULATE(TEXTURE, CURRENT), 0x0059c549) — the framebuffer blend weight a delayed-alpha subset composites with — is not modelled; acdream instead draws a second pass weighted by detail.a*diffuseAlpha. Identical for opaque subsets, a bounded difference on translucent building/EnvCell subsets already covered by the existing AP-34 shared-alpha-queue divergence row. Also qualified the tmpmaterial.Diffuse.a = 1f (0x0059cb99) citation to name its exact branch (burnedInStaticLights < 0 && *(render_device+0x7e4) == 0); the other branch leaves diffuse FromVertex, but the opaque->1 / fading->opacity mapping still holds either way. Nit also folded in: EnvCellRendererTests' new SubmitRhi instance-alpha test is now a [Theory] over WbRenderPass.Opaque and .Transparent, pinning the bind-before-first-draw invariant on both passes. Regenerated mesh_detail's committed SPIR-V and the shader manifest (tools/compile-shaders.ps1); no other shader pair changed. Verified: dotnet build AcDream.slnx -c Release (0 warnings, 0 errors); dotnet test on AcDream.App.Tests (Release, hermetic lanes) green, including the shader manifest tests explicitly; AcDream.Core.Tests unaffected/green. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
112 lines
3.9 KiB
GLSL
112 lines
3.9 KiB
GLSL
#version 430 core
|
|
#extension GL_ARB_shader_draw_parameters : require
|
|
|
|
layout(location = 0) in vec3 aPosition;
|
|
layout(location = 1) in vec3 aNormal;
|
|
layout(location = 2) in vec2 aTexCoord;
|
|
|
|
struct InstanceData {
|
|
mat4 transform;
|
|
};
|
|
|
|
struct BatchData {
|
|
uint textureIndex;
|
|
uint _pad;
|
|
uint textureLayer;
|
|
uint flags;
|
|
};
|
|
|
|
layout(std430, binding = 0) readonly buffer InstanceBuffer {
|
|
InstanceData Instances[];
|
|
};
|
|
layout(std430, binding = 1) readonly buffer BatchBuffer {
|
|
BatchData Batches[];
|
|
};
|
|
|
|
struct CellClip {
|
|
uint count;
|
|
uint _p0;
|
|
uint _p1;
|
|
uint _p2;
|
|
vec4 planes[8];
|
|
};
|
|
layout(std430, binding = 2) readonly buffer ClipRegionBuf {
|
|
CellClip clipRegions[];
|
|
};
|
|
layout(std430, binding = 3) readonly buffer ClipSlotBuf {
|
|
uint instanceClipSlot[];
|
|
};
|
|
|
|
// Object renderer only: 1 for a retail building shell, 0 for ordinary
|
|
// scenery/creatures/players. EnvCellRenderer sets uParamB=0 and ignores the
|
|
// value, but still binds one valid word because Vulkan sees this static use.
|
|
layout(std430, binding = 9) readonly buffer InstanceDetailCategoryBuf {
|
|
uint instanceDetailCategory[];
|
|
};
|
|
|
|
// #188 per-instance opacity multiplier, identical binding and indexing to
|
|
// mesh_modern.vert's InstanceAlphaBuf (binding 7). VM2's cdb read proved
|
|
// retail's built-mesh detail combine is a single-pass texture-stage blend
|
|
// whose stage-0 alpha is PREMODULATE(DIFFUSE, DIFFUSE) = diffuse.a *
|
|
// detail.a (D3DPolyRender::SetSurface 0x0059c4d0) — the base subset's own
|
|
// diffuse alpha gates how much detail shows through, exactly like the base
|
|
// pass's translucency-fade multiplier already does for mesh_modern. 1.0 for
|
|
// every opaque subset; <1.0 while a TransparentPartHook fade is in flight.
|
|
layout(std430, binding = 7) readonly buffer InstanceAlphaBuf {
|
|
float instanceAlpha[];
|
|
};
|
|
|
|
out gl_PerVertex {
|
|
vec4 gl_Position;
|
|
float gl_ClipDistance[8];
|
|
};
|
|
|
|
uniform mat4 uViewProjection;
|
|
uniform int uDrawIDOffset;
|
|
uniform uint uTextureIndexB; // absolute transform prefix in the shared pose arena
|
|
uniform float uParamA; // detail UV tiling
|
|
uniform float uParamB; // 1 = require building instance, 0 = EnvCell category
|
|
|
|
out vec2 vBaseUv;
|
|
out vec2 vDetailUv;
|
|
out float vDetailOpacity;
|
|
out vec3 vWorldPos; // review fix: mesh_detail.frag needs this for applyFog,
|
|
// exactly like mesh_modern.vert's vWorldPos.
|
|
out flat uint vBaseTextureIndex;
|
|
out flat uint vBaseTextureLayer;
|
|
out flat uint vBatchFlags;
|
|
out flat uint vDetailCategory;
|
|
|
|
void main() {
|
|
int transformIndex = gl_BaseInstanceARB + gl_InstanceID;
|
|
int instanceIndex = transformIndex - int(uTextureIndexB);
|
|
vec4 worldPos = Instances[transformIndex].transform * vec4(aPosition, 1.0);
|
|
gl_Position = uViewProjection * worldPos;
|
|
vWorldPos = worldPos.xyz;
|
|
|
|
uint slot = instanceClipSlot[instanceIndex];
|
|
CellClip clip = clipRegions[slot];
|
|
for (uint i = 0u; i < clip.count; ++i)
|
|
gl_ClipDistance[i] = dot(clip.planes[i], gl_Position);
|
|
for (uint i = clip.count; i < 8u; ++i)
|
|
gl_ClipDistance[i] = 1.0;
|
|
|
|
// No distance term: VM2 found the fade only exists in
|
|
// D3DPolyRender::DrawPolyInternal (0x0059d7c0, the immediate-polygon
|
|
// path) and only when the static noFadeDetail (0x00820e38, initialised
|
|
// to 1) is 0. Every loaded CGfxObj sets use_built_mesh=1
|
|
// (CGfxObj::InitLoad 0x005346b0), so buildings/EnvCells never reach that
|
|
// function; their attenuation is the LINEAR mip chain converging to the
|
|
// texture mean, which the existing sampler already provides.
|
|
vDetailOpacity = instanceAlpha[instanceIndex];
|
|
vBaseUv = aTexCoord;
|
|
vDetailUv = aTexCoord * uParamA;
|
|
|
|
BatchData batch = Batches[uDrawIDOffset + gl_DrawIDARB];
|
|
vBaseTextureIndex = batch.textureIndex;
|
|
vBaseTextureLayer = batch.textureLayer;
|
|
vBatchFlags = batch.flags;
|
|
vDetailCategory = uParamB > 0.5
|
|
? instanceDetailCategory[instanceIndex]
|
|
: 1u;
|
|
}
|