Commit 2 deleted the GL rendering backend's implementations; this step removes the package references and shader vocabulary they leave behind, so nothing in the App project still spells Silk.NET.OpenGL. Silk.NET.OpenGL and Silk.NET.OpenGL.Extensions.ARB are dropped from AcDream.App.csproj. Chorizite.Core stays — the audit is NOT clean: its Render.Enums (TextureFormat, BufferUsage) and Lib.BoundingBox types are used directly and extensively across the Wb texture/mesh pipeline, independent of the deleted GL IUniformBuffer implementers the package comment used to cite. The stale comment is corrected in place. IMeshPipelineDevice.Gl is removed along with the GL? gl parameter threaded through WbMeshAdapter's four constructors, WorldRenderComposition's CreateMeshAdapter, and VulkanMeshPipelineDevice's Gl => null implementation — nothing read any of them once the legacy per-mesh upload bodies were gone (confirmed by grep: the sole non-doc-comment hit was a test assertion). While in WbMeshAdapter.Dispose(), found and fixed a real bug along the way: its teardown still pattern-matched the deleted GL GpuFrameFlightController to decide whether to wait for submitted work, which VulkanFrameFlightController replaced at slice V6a without this site being updated — so the wait had been silently dead on every Vulkan run since then. Retargeted to VulkanFrameFlightController, which carries the same WaitForSubmittedWork(). The GL pixel-format vocabulary (Silk.NET.OpenGL.PixelFormat/PixelType) that WorldTextureArray/TextureFormatExtensions/TextureAtlasManager used for upload validation is replaced by AcDream.Content's existing Silk.NET-free UploadPixelFormat/UploadPixelType enums (added at MP1a to keep the bake tool GL-free); two new members (Rgb, Red, Float) extend that enum with their GL ABI constants to cover the full vocabulary WorldTextureArray needs, since MP1a's original set only covered what the extractor itself emits. ObjectMeshManager's App-boundary cast `(Silk.NET.OpenGL.PixelFormat?)batch.UploadPixelFormat` becomes a direct pass-through now that both sides share the type. GpuBindingModel.StorageTextureTable (the GL-only binding=9 emulation of the Vulkan texture table) is deleted and StorageBindingCount drops from 10 to 9; the descriptor-set-layout code that builds from that count (VulkanPipelineLayouts, VulkanFrameBindings) is untouched and just allocates one fewer always-dummy-seeded, always-unused binding. Several fully dead GL-only classes came along for the ride, confirmed by zero construction sites: SilkFramebufferViewportTarget (NullFramebufferViewportTarget is the sole production IFramebufferViewportTarget), SilkRenderGlStateReader (NullRenderGlStateReader.Instance is the sole IRenderGlStateReader), RuntimeRenderFrameClearPhase (VulkanRenderFrameClearPhase is the sole IRenderFrameClearPhase, expressing the same atmosphere-clear logic as a pass load-op instead), and GpuFrameTimer plus FrameProfiler's GL-owning FrameBoundary(GL) overload and BeginGpuFrame/EndGpuFrame bracket (RecordGpuSample is the only GPU-timing path any backend uses now — the ACDREAM_WB_DIAG nested-query exclusion these existed for no longer applies, since WbDrawDispatcher's own diagnostic GPU sampling already moved to the device's Vulkan timer pool). GpuFrameFlightController itself stays (never constructed with a real fence API in production, but its retirement-ledger/serial-ring logic is backend-neutral and still covered by its own unit tests) — only its GL-specific parts (the public GL constructor overload, SilkGpuFenceApi) are deleted, since removing the whole class would mean restructuring the frozen Slice-8 composition shape's GpuFrameFlightController? threading, which is out of this commit's scope. TextureParameters.cs and BufferUsageExtensions.cs (zero callers each) are deleted outright. common.glsl is deleted: nothing in the actual Vulkan .spv build reads it. tools/ShaderCompiler/Program.cs compiles each .vert/.frag pair directly and tools/ShaderCompiler/VulkanGlslPreamble.cs injects its own complete self-contained preamble per file; common.glsl's textual concatenation was exclusively Shader.cs's GL-only mechanism, deleted at Commit 2. The five shader files that named it in comments (mesh_modern.vert, particle.vert, particle.frag, sky.frag, terrain_modern.frag) are corrected to point at VulkanGlslPreamble.cs instead. mesh.vert/mesh.frag — the pre-N.5 legacy shader pair the mandatory modern path already made unreachable, with zero C# consumers and no compiled .spv — are deleted too. Regenerated via tools/compile-shaders.ps1: 9/9 remaining shader pairs compile (previously 9/10, with mesh the sole failure — the VulkanShaderManifestTests doc comment's "nine of ten are not Vulkan-expressible" was already stale before this commit). Test fallout: dead-subject test methods/files are deleted rather than patched (TextRendererFailureSafetyTests.cs, ClipFrameUploadTests.cs, GpuResourceRetirementTransactionTests.cs's GL queue tests, one WorldRenderDiagnosticsTests source-order test, one RenderFrameResourceControllerTests clear-phase-order test); tests whose subject moved or was renamed are updated in place rather than deleted (GpuContractTests, VulkanCapabilityGateTests, MeshPipelineDeviceSeamTests' pinned seven-member surface now reads six, ParticleBindlessInstanceTests' cross-dialect check now covers the one surviving dialect, WbMeshAdapterTests' misleadingly-named null-gl test — gpuDevice was always the parameter that actually threw). Build: `dotnet build AcDream.slnx -c Release` — 0 warnings, 0 errors, with the Silk.NET.OpenGL/.Extensions.ARB package references physically removed from the csproj (not just unreferenced in code). Tests: full-solution `dotnet test` green across every project. Zero remaining `using Silk.NET.OpenGL` anywhere in src/ or tests/. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
337 lines
17 KiB
GLSL
337 lines
17 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;
|
||
};
|
||
|
||
// Campaign V slice V2 (2026-07-27): textureHandle (uvec2, a 64-bit
|
||
// GL_ARB_bindless_texture handle) became textureIndex (uint) plus an explicit
|
||
// pad word. textureIndex is a slot into the global texture table (set 2,
|
||
// injected by tools/ShaderCompiler/VulkanGlslPreamble.cs — see
|
||
// ACDREAM_TEXTURE_HANDLE/ACDREAM_SAMPLE_ARRAY) which main() below forwards to
|
||
// the fragment stage. The pad word keeps textureLayer/flags at their original
|
||
// std430 offsets (8/12), so the struct is still 16 bytes and every existing
|
||
// CPU writer's layout is unchanged (GpuBindingModel.GpuBatchDataStrideBytes).
|
||
struct BatchData {
|
||
uint textureIndex; // slot into the global texture table
|
||
uint _pad; // keeps textureLayer/flags at offsets 8/12
|
||
uint textureLayer; // layer in the shared WB or pooled composite array
|
||
uint flags; // reserved — N.5 dispatcher owns all blend state
|
||
// (glBlendFunc per pass). If a future phase wants
|
||
// shader-side per-batch additive flag (Decision 2
|
||
// fallback), encode it here as bit 0.
|
||
};
|
||
|
||
layout(std430, binding = 0) readonly buffer InstanceBuffer {
|
||
InstanceData Instances[];
|
||
};
|
||
|
||
// binding=1 here is the SSBO namespace — distinct from the UBO namespace.
|
||
// SceneLighting UBO also uses binding=1 in the fragment shader; GL keeps
|
||
// GL_SHADER_STORAGE_BUFFER and GL_UNIFORM_BUFFER binding tables separate.
|
||
// Task 10 dispatcher binds:
|
||
// glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, instanceSsbo)
|
||
// glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, batchSsbo)
|
||
// Existing SceneLightingUboBinding handles the UBO side.
|
||
layout(std430, binding = 1) readonly buffer BatchBuffer {
|
||
BatchData Batches[];
|
||
};
|
||
|
||
// === Phase U.3: per-cell screen-space clip gate (gl_ClipDistance) =============
|
||
// Two SSBOs add the clip mechanism without disturbing binding=0/1 above.
|
||
//
|
||
// binding=2 — SHARED per-frame clip regions, one CellClip per "slot". Uploaded
|
||
// ONCE per frame by ClipFrame.UploadShared (shared across WbDrawDispatcher +
|
||
// EnvCellRenderer). Slot 0 is RESERVED = no-clip (count 0 ⇒ every plane passes).
|
||
//
|
||
// binding=3 — PER-RENDERER per-instance slot index, parallel to the binding=0
|
||
// instance buffer and indexed by the IDENTICAL per-instance index
|
||
// (gl_BaseInstanceARB + gl_InstanceID). instanceClipSlot[i] selects which
|
||
// CellClip region instance i is clipped against. Default all-zeros in U.3 ⇒
|
||
// every instance maps to slot 0 ⇒ no clipping ⇒ identical render to pre-U.3.
|
||
//
|
||
// CellClip std430 layout (144 bytes/slot): a uint count + 3 pad uints (16 bytes)
|
||
// then vec4 planes[8] (8 × 16 = 128 bytes). vec4 array stride is 16 under std430.
|
||
// ClipFrame on the CPU side lays out the bytes to match exactly (verified by
|
||
// ClipFrameLayoutTests). A clip-space vertex is INSIDE iff dot(plane, gl_Position)
|
||
// >= 0 for every active plane (see ClipPlaneSet for the plane convention).
|
||
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[];
|
||
};
|
||
|
||
// === Fix B (A7 #3): per-OBJECT light selection — minimize_object_lighting =====
|
||
// retail picks up-to-8 point/spot lights PER OBJECT by the object's own position
|
||
// (minimize_object_lighting 0x0054d480), so a torch always lights the wall it
|
||
// sits on, camera-INDEPENDENTLY. The previous single global nearest-8-to-CAMERA
|
||
// UBO set (LightManager.Tick) made a wall brighten as the camera approached
|
||
// (its torches swapping into the global top-8). Two SSBOs replace that for
|
||
// point/spot lights (the SUN + ambient still come from the SceneLighting UBO):
|
||
//
|
||
// binding=4 — GLOBAL point/spot light array, uploaded once per frame from
|
||
// LightManager.PointSnapshot. The index of a light here is stable for the frame.
|
||
// binding=5 — per-instance light SET: MaxLightsPerObject(8) int indices per
|
||
// instance INTO gLights[] (-1 = unused slot), parallel to the binding=0
|
||
// instance buffer and indexed by the SAME instanceIndex. WbDrawDispatcher fills
|
||
// it once per entity (the set is constant across the entity's parts/tuples).
|
||
struct GlobalLight {
|
||
vec4 posAndKind;
|
||
vec4 dirAndRange;
|
||
vec4 colorAndIntensity;
|
||
vec4 coneAngleEtc;
|
||
};
|
||
layout(std430, binding = 4) readonly buffer GlobalLightBuf {
|
||
GlobalLight gLights[];
|
||
};
|
||
layout(std430, binding = 5) readonly buffer InstanceLightSetBuf {
|
||
int instanceLightIdx[]; // 8 per instance; -1 = unused
|
||
};
|
||
|
||
// #142: per-instance "indoor" flag, 1 per instance, parallel to the binding=0
|
||
// instance buffer (same instanceIndex). 1 = object parented to an EnvCell (skip the
|
||
// sun — retail's useSunlight==0 interior stage); 0 = outdoor object (gets the sun).
|
||
// Read ONLY inside the uniform `uLightingMode == 0` branch below, so the mode-1
|
||
// (EnvCell shell) path provably never touches it — EnvCellRenderer need not bind it.
|
||
layout(std430, binding = 6) readonly buffer InstanceIndoorBuf {
|
||
uint instanceIndoor[];
|
||
};
|
||
|
||
// #188: per-instance opacity multiplier, 1 per instance, parallel to the
|
||
// binding=0 instance buffer (same instanceIndex). 1.0 = unmodified; <1.0
|
||
// while a TransparentPartHook translucency fade is in flight for the
|
||
// entity/part this instance belongs to (e.g. the "fading wall" secret-
|
||
// passage doors). Multiplied against the sampled texture alpha in
|
||
// mesh_modern.frag.
|
||
layout(std430, binding = 7) readonly buffer InstanceAlphaBuf {
|
||
float instanceAlpha[];
|
||
};
|
||
|
||
// Retail SmartBox click confirmation. One vec2 per OBJECT instance, parallel
|
||
// to binding=0: x = CMaterial luminosity, y = CMaterial diffuse. Normal
|
||
// rendering is (0,1); SmartBox alternates LOW=(0,.35) and HIGH=(.99,1).
|
||
// EnvCellRenderer uses uLightingMode=1 and deliberately never reads this
|
||
// object-only binding.
|
||
layout(std430, binding = 8) readonly buffer InstanceSelectionLightingBuf {
|
||
vec2 instanceSelectionLighting[];
|
||
};
|
||
|
||
// Core profile: redeclare gl_PerVertex so writing gl_ClipDistance[] is legal
|
||
// alongside gl_Position. The array is sized 8 to match the CellClip plane budget
|
||
// and the GL guarantee (GL_MAX_CLIP_DISTANCES >= 8). The host enables
|
||
// GL_CLIP_DISTANCE0..7 once at startup; unused planes are set to +1.0 below so
|
||
// they pass everything (no clipping) when the slot's count < 8.
|
||
out gl_PerVertex {
|
||
vec4 gl_Position;
|
||
float gl_ClipDistance[8];
|
||
};
|
||
|
||
uniform mat4 uViewProjection;
|
||
|
||
// Phase Post-A.5 (ISSUE #52, 2026-05-10): per-pass offset into Batches[].
|
||
// gl_DrawIDARB resets to 0 at the start of each glMultiDrawElementsIndirect
|
||
// call, so the transparent pass — which begins later in the indirect buffer
|
||
// — was fetching Batches[0..transparentCount) instead of its actual section
|
||
// at Batches[opaqueCount..end). The lifestone crystal (a transparent draw)
|
||
// ended up reading the FIRST OPAQUE batch's TextureHandle every frame. As
|
||
// the camera moved and the opaque front-to-back sort reordered which group
|
||
// landed at BatchData[0], the lifestone's apparent texture flickered to
|
||
// whatever was first — frequently the player character's body parts.
|
||
//
|
||
// WbDrawDispatcher.Draw sets this to 0 before the opaque MDI call and to
|
||
// _opaqueDrawCount before the transparent MDI call, matching WorldBuilder's
|
||
// uDrawIDOffset pattern in BaseObjectRenderManager.cs line 845.
|
||
uniform int uDrawIDOffset;
|
||
uniform int uLightingMode; // A7 Fix D: 0 = OBJECT (plain Lambert + sun), 1 = ENVCELL (half-Lambert wrap, no sun)
|
||
// #176 stripe-hunt isolation modes (ACDREAM_LIGHT_DEBUG, throwaway diagnostic):
|
||
// 0 = off; 1 = ambient-only vLit (all point/sun contributions killed);
|
||
// 2 = DYNAMIC point lights killed (purples + viewer fill off, statics stay);
|
||
// 3 = handled in the frag (raw vLit visualization, texture ignored).
|
||
uniform int uLightDebug;
|
||
|
||
// SceneLighting UBO — binding=1 in the UBO namespace (GL keeps the SSBO and UBO
|
||
// binding tables separate, so this coexists with the binding=1 BatchBuffer SSBO
|
||
// above). IDENTICAL std140 layout to mesh_modern.frag.
|
||
//
|
||
// A7 (2026-06-15): lighting moved from the FRAGMENT shader to HERE (per-VERTEX) so
|
||
// torch/point lights Gouraud-interpolate across each triangle the way retail's
|
||
// fixed-function T&L does (D3D DrawEnvCell vertex bake + minimize_object_lighting for
|
||
// objects). A per-PIXEL evaluation made a tight bright "spotlight" pool on flat walls;
|
||
// per-vertex spreads it into a soft, broad gradient with no hard edge.
|
||
struct Light {
|
||
vec4 posAndKind;
|
||
vec4 dirAndRange;
|
||
vec4 colorAndIntensity;
|
||
vec4 coneAngleEtc;
|
||
};
|
||
layout(std140, ACDREAM_UBO_SET binding = 1) uniform SceneLighting {
|
||
Light uLights[8];
|
||
vec4 uCellAmbient;
|
||
vec4 uFogParams;
|
||
vec4 uFogColor;
|
||
vec4 uCameraAndTime;
|
||
};
|
||
|
||
// Faithful calc_point_light (0x0059c8b0) contribution from ONE point/spot light —
|
||
// the wrap + norm shape, factored out so the per-object SSBO loop shares it. D =
|
||
// light − vertex, used UN-normalised (length = dist); N is the unit vertex normal.
|
||
// Returns the RGB to ADD, already per-channel capped to the light's own colour.
|
||
vec3 pointContribution(vec3 N, vec3 worldPos, GlobalLight L) {
|
||
int kind = int(L.posAndKind.w);
|
||
vec3 toL = L.posAndKind.xyz - worldPos; // D (un-normalised)
|
||
float distsq = dot(toL, toL);
|
||
float d = sqrt(distsq);
|
||
float range = L.dirAndRange.w; // falloff_eff = Falloff × 1.3 (static) / × 1.5 (dynamic)
|
||
if (d >= range || range <= 1e-4) return vec3(0.0);
|
||
float intensity = L.colorAndIntensity.w;
|
||
vec3 baseCol = L.colorAndIntensity.xyz;
|
||
|
||
// #143: DYNAMIC lights (viewer fill, portal, server-object lights — flagged by
|
||
// coneAngleEtc.y==1 from GlobalLightPacker) use retail's D3D hardware attenuation
|
||
// (config_hardware_light 0x0059ad30): a POINT light is given Attenuation1=1 ⇒
|
||
// att = 1/d (inverse-LINEAR), plain Lambert N·L, hard range cutoff. That spreads
|
||
// softly across the room (the portal tint, the viewer fill) instead of the static
|
||
// bake's 1/d³ distance-cube, which makes a tight concentrated pool. No per-light
|
||
// cap — D3D accumulates then saturates, which accumulateLights does via min(pointAcc,1).
|
||
if (L.coneAngleEtc.y > 0.5) {
|
||
if (uLightDebug == 2) return vec3(0.0); // #176 stripe hunt: dynamics killed
|
||
vec3 Ldir = toL / max(d, 1e-4);
|
||
float ndl = max(0.0, dot(N, Ldir));
|
||
if (ndl <= 0.0) return vec3(0.0);
|
||
if (kind == 2) { // dynamic spot: hard cos-cone gate
|
||
if (dot(-Ldir, L.dirAndRange.xyz) <= cos(L.coneAngleEtc.x * 0.5)) return vec3(0.0);
|
||
}
|
||
return (intensity * ndl / max(d, 1e-3)) * baseCol; // att = 1/d
|
||
}
|
||
|
||
// ── STATIC dat-baked lights: retail's per-vertex bake (calc_point_light 0x0059c8b0) ──
|
||
// A7 Fix D D-3: angular term by lighting path. ENVCELL bake (mode 1) keeps the
|
||
// half-Lambert wrap (lights surfaces angled away, retail calc_point_light); OBJECT
|
||
// mode (0) uses plain Lambert max(0,N·L) so a torch BEHIND a character contributes
|
||
// nothing (retail's hardware path). toL is un-normalised (length d).
|
||
float angular = (uLightingMode == 1)
|
||
? (1.0 / 1.5) * (dot(N, toL) + 0.5 * d) // half-Lambert wrap (EnvCell bake)
|
||
: max(0.0, dot(N, toL)); // plain Lambert (object/hardware)
|
||
if (angular <= 0.0) return vec3(0.0);
|
||
// NORM branch (distance-cube): >1 m → distsq·d ≈ inverse-square soft far halo;
|
||
// <1 m → just d (dodge the near singularity). "Punchy near, soft far."
|
||
float norm = (distsq > 1.0) ? (distsq * d) : d;
|
||
float scale = (1.0 - d / range) * intensity * (angular / norm);
|
||
if (kind == 2) {
|
||
// Spotlight: hard-edged cos-cone gate layered on the point ramp.
|
||
vec3 Ldir = toL / max(d, 1e-4);
|
||
float cos_edge = cos(L.coneAngleEtc.x * 0.5);
|
||
float cos_l = dot(-Ldir, L.dirAndRange.xyz);
|
||
if (cos_l <= cos_edge) scale = 0.0;
|
||
}
|
||
// Per-channel no-blowout cap to the light's OWN colour (un-intensity-scaled):
|
||
// a single light can't push a channel past its colour. Summed lit clamped in frag.
|
||
return min(scale * baseCol, baseCol);
|
||
}
|
||
|
||
vec3 accumulateLights(vec3 N, vec3 worldPos, int instanceIndex) {
|
||
vec3 lit = uCellAmbient.xyz;
|
||
if (uLightDebug == 1) return lit; // #176 stripe hunt: ambient only
|
||
|
||
// SUN / directional — OBJECT path only (mode 0). retail's EnvCell path
|
||
// (minimize_envcell_lighting) enables only dynamic lights, NEVER the sun, so
|
||
// EnvCell walls (mode 1) get no directional sun wash (A7 Fix D D-4).
|
||
// #142: within mode 0, also skip the sun for indoor objects (ParentCellId is an
|
||
// EnvCell). This mirrors retail's per-draw-stage useSunlight toggle: the interior
|
||
// stage runs useSunlightSet(0) (PView::DrawCells 0x005a49f3), so indoor objects
|
||
// get no sun even in windowed buildings where the player's frame is not sun-killed.
|
||
if (uLightingMode == 0) {
|
||
if (instanceIndoor[instanceIndex] == 0u) { // #142: outdoor objects only get the sun
|
||
int activeLights = int(uCellAmbient.w);
|
||
for (int i = 0; i < 8; ++i) {
|
||
if (i >= activeLights) break;
|
||
if (int(uLights[i].posAndKind.w) != 0) continue; // directional only
|
||
vec3 Ldir = -uLights[i].dirAndRange.xyz;
|
||
float ndl = max(0.0, dot(N, Ldir));
|
||
lit += uLights[i].colorAndIntensity.xyz * uLights[i].colorAndIntensity.w * ndl;
|
||
}
|
||
}
|
||
}
|
||
|
||
// POINT / SPOT torches: their OWN accumulator (A7 Fix D, D-1). Retail's
|
||
// SetStaticLightingVertexColors sums the static point lights from BLACK and
|
||
// clamps the SUM to [0,1] before anything else (a baked emissive term), so a
|
||
// few warm intensity-100 torches can't push the whole pixel to white the way
|
||
// folding them into ambient+sun did. Mirrors LightBake.ComputeVertexColor
|
||
// (LightBakeConformanceTests). Per-light cap inside pointContribution is unchanged.
|
||
vec3 pointAcc = vec3(0.0);
|
||
int base = instanceIndex * 8;
|
||
for (int k = 0; k < 8; ++k) {
|
||
int gi = instanceLightIdx[base + k];
|
||
if (gi < 0) continue;
|
||
pointAcc += pointContribution(N, worldPos, gLights[gi]);
|
||
}
|
||
lit += min(pointAcc, vec3(1.0)); // clamp the torch sum on its own (retail baked emissive)
|
||
|
||
return lit; // frag still does the final min(lit, 1.0)
|
||
}
|
||
|
||
out vec3 vNormal;
|
||
out vec2 vTexCoord;
|
||
out vec3 vWorldPos;
|
||
out vec3 vLit; // A7: per-vertex Gouraud lighting (ambient + capped lights)
|
||
// Campaign V slice V6e: was `flat uvec2 vTextureHandle` — a raw 64-bit
|
||
// GL_ARB_bindless_texture handle handed across the stage boundary. A varying
|
||
// cannot carry a Vulkan descriptor, so what travels is the table SLOT and the
|
||
// fragment stage does the lookup (see mesh_modern.frag). Under GL the value
|
||
// sampled is bit-for-bit the one the vertex stage used to forward; the SSBO
|
||
// read simply happens one stage later, and `flat` keeps it one scalar load per
|
||
// primitive rather than per fragment.
|
||
out flat uint vTextureIndex;
|
||
out flat uint vTextureLayer;
|
||
out flat float vOpacityMultiplier; // #188
|
||
out flat vec2 vSelectionLighting;
|
||
|
||
void main() {
|
||
int instanceIndex = gl_BaseInstanceARB + gl_InstanceID;
|
||
mat4 model = Instances[instanceIndex].transform;
|
||
vOpacityMultiplier = instanceAlpha[instanceIndex]; // #188
|
||
vSelectionLighting = (uLightingMode == 0)
|
||
? instanceSelectionLighting[instanceIndex]
|
||
: vec2(0.0, 1.0);
|
||
|
||
vec4 worldPos = model * vec4(aPosition, 1.0);
|
||
gl_Position = uViewProjection * worldPos;
|
||
|
||
// Phase U.3: per-instance clip gate. instanceClipSlot is indexed by the
|
||
// SAME instanceIndex used for the binding=0 transform above, so the slot
|
||
// travels with the instance through the MDI BaseInstance offsets. Slot 0
|
||
// (the U.3 default) has count 0 ⇒ the second loop sets all 8 distances to
|
||
// +1.0 ⇒ nothing is clipped.
|
||
uint _slot = instanceClipSlot[instanceIndex];
|
||
CellClip _c = clipRegions[_slot];
|
||
for (uint i = 0u; i < _c.count; ++i)
|
||
gl_ClipDistance[i] = dot(_c.planes[i], gl_Position);
|
||
for (uint i = _c.count; i < 8u; ++i)
|
||
gl_ClipDistance[i] = 1.0;
|
||
|
||
vWorldPos = worldPos.xyz;
|
||
vNormal = normalize(mat3(model) * aNormal);
|
||
vLit = accumulateLights(vNormal, vWorldPos, instanceIndex); // A7: per-vertex Gouraud (per-object lights)
|
||
vTexCoord = aTexCoord;
|
||
|
||
BatchData b = Batches[uDrawIDOffset + gl_DrawIDARB];
|
||
// Campaign V slice V6e: forward the table SLOT untouched. V2 looked the
|
||
// handle up here and passed the handle; the lookup now lives at the sample
|
||
// site in mesh_modern.frag, which is the only form Vulkan can express.
|
||
vTextureIndex = b.textureIndex;
|
||
vTextureLayer = b.textureLayer;
|
||
}
|