Feature/physics#47
Open
ViPErCZ wants to merge 42 commits into
Open
Conversation
Phase B1 of shader-system refactor towards Limitless-style composition. Additive only - no behavior change for existing shaders. - ShaderFeature enum + bitmask for future #ifdef-driven permutations - ShaderRegistry as future single source of truth for programs, with FNV-1a 64bit permutation cache and lazy compilation in get() - ShaderPreprocessor for #define X injection between #version and source - ShaderLoader: stateful include resolver with double-include guard and GLSL #line directives backed by an int file table - App registers all sync shaders in registry parallel to ResourceManager (no GL compilation yet - registration is just path metadata) Build: Snake3 + Tests green, ctest 16/16 passing. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase B2 of shader-system refactor. ShaderRegistry now produces real permutation-cached programs with #define injection. - ShaderLoader::loadShaderSource: public method returning #include-resolved source as string, so ShaderRegistry can preprocess before compile - ShaderRegistry::get: removed assert(features==0); loads VS/FS strings, injects #define FEATURE_X via ShaderPreprocessor, compiles via bindFromBuffer. Vertex-only (transform feedback) shaders bypass preprocessor since particle updates don't use feature flags - Smoke test in App::Init (IS_DEBUG only): verifies cache hit for same handle and cache miss across feature masks. Compiles basicShader twice extra under debug; this duplication disappears in B6 when registry replaces ResourceManager::addShader as single source of truth B3 will add the matching `#ifdef FEATURE_*` blocks inside basic.fs so the injected defines actually affect rendering. Until then, masked variants compile to functionally identical programs. Build: Snake3 + Tests green, ctest 16/16 passing. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…(B3a) Phase B3a: wrap PBR/NormalMap/Shadows/IBL/Fog/DirectionalLight blocks in basic.fs with `#ifdef FEATURE_*`. Else-branches that contain real fallbacks (Phong, alphaBlending, regular point lights) are split into separate `if (!cond)` so they stay available when the feature is off. basicShader compilation moves from direct ShaderLoader to ShaderRegistry::get with a legacy mask containing all features. With all features active the new #ifdef blocks behave identically to the previous runtime-only code. The duplication shrinks in B6 once materials build through MaterialBuilder. Discipline for B5+: C++ must not set xEnable=true on a shader that was compiled without FEATURE_X - the if-branch would be stripped but the !cond fallback would also skip, leaving the value uninitialised. The upcoming MaterialFeature classes enforce this by only setting uniforms when the feature is in the mask. basic.vs, plane.fs and other shaders sharing basic.vs (respawnShader, rainDrop) are untouched in this PR - they get migrated in B3b and B3d. Build: Snake3 + Tests green, ctest 16/16 passing. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`add_custom_target(copy_assets ALL ...)` only registers the target with the default ALL build, so `cmake --build --target Snake3` skips it and partial rebuilds keep running with stale shaders. Adding the explicit dependency ensures every Snake3 build refreshes build/Assets. Discovered while diagnosing a shadow regression that turned out to be caused by partial rebuilds shipping with assets from a previous commit. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase B3b: gate the skeletal vertex transform in basic.vs with `#ifdef FEATURE_BONES`. Same safety-net pattern as basic.fs - the bone path stays inside `#ifdef`, the non-bones fallback runs through a parallel `if (!useBones)`. - basic.vs: useBones default changes from true to false. With the new split, a shader compiled without FEATURE_BONES and a caller that forgets to set useBones would otherwise leave gl_Position unwritten (bones branch stripped, !useBones false). All current C++ paths set useBones explicitly (StandardMaterial::bind=false, AnimationArrayMesh=true, snake respawn=false), so the default flip is inert in practice. - App.cpp: Bones added to legacyBasicFeatures so basicShader keeps both paths compiled. - App.cpp: planeShader migrates from direct ShaderLoader to registry get with the same feature mask. plane.fs is untouched (its main() doesn't reference any FEATURE_*), but it shares basic.vs which now needs the Bones flag for the legacy vertex path. - respawnShader and rainDrop stay async without features. Their callers set useBones=false, so the non-bones path is exercised exclusively and the bones-branch absence has no effect. basic.fs and lights.glsl are unchanged. B3c will optionally tighten lights.glsl (PBR/IBL function gating). Build: Snake3 + Tests green, ctest 16/16 passing. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase B3c: wrap PBR/IBL function prototypes and bodies in lights.glsl with `#ifdef FEATURE_PBR` / `#ifdef FEATURE_IBL`. Same defines pattern as the call sites in basic.fs - when the feature is off the entire helper disappears from the compiled program. Affected functions: - FEATURE_PBR: CalcDirLightPBR, CalcPointLightPBR, CalcSpotLightPBR (prototype only; impl was already commented out), fresnelSchlick, DistributionGGX, GeometrySchlickGGX, GeometrySmith - FEATURE_IBL: CalcIBLSpecular, CalcIBLDiffuse Non-PBR helpers (CalcDirLight, CalcDirLightMaterial, CalcPointLight, CalcSpotLight, computePulse) stay unconditional - they're used by non-PBR shaders too (shadow_map.fs, normal_map.fs, respawn.fs, explosion.fs all include lights.glsl for these and the struct definitions). Verified none of the always-on lights.glsl consumers reference the gated functions, so the #ifdef wrap is safe for legacy paths. Build: Snake3 + Tests green, ctest 16/16 passing. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase B3d removes the plane.fs duplicate. plane.fs was a verbatim copy of basic.fs with an extra hole-map discard block; now that basic.fs supports feature gating, the hole map lives there too. - ShaderFeature::HoleMap (bit 11), featureName "FEATURE_HOLE_MAP" - basic.fs: hasHoleMap / holeMap uniforms declared unconditionally so C++ setBool/setInt calls always land somewhere; the actual discard branch is wrapped in `#ifdef FEATURE_HOLE_MAP` so non-plane shaders pay nothing - App.cpp: planeShader is no longer a separate master. The ResourceManager alias stays for backward compat with MainScene::initPlane and PlaneMaterial - both still ask for "planeShader", but it now resolves to a basicShader permutation with legacyBasicFeatures | HoleMap - Assets/Shaders/plane.fs deleted PlaneMaterial::bind already binds holeMap to texture unit 8 and sets hasHoleMap=true at runtime, so no code change is required there. This is the first concrete payoff of the B refactor - 180+ lines of duplicated shader replaced by a 13-line #ifdef block. Build: Snake3 + Tests green, ctest 16/16 passing. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase B3e wires the snippet-injection infrastructure. No runtime behavior change - the registry still doesn't call injectSnippet, that happens in B5 once MaterialBuilder exists. - basic.fs gets a `// @MATERIAL_FRAGMENT_PRE` marker at the top of main(). Until B5, it's just a GLSL comment so compilation is unchanged. - ShaderPreprocessor::injectSnippet(source, marker, snippet) replaces the entire line containing the marker with snippet content. Adds a trailing newline if the snippet lacks one. Missing marker / empty snippet are silent no-ops. Only the first occurrence is replaced - later occurrences stay as markers for future call sites. - Eight Catch2 cases in Tests/ShaderPreprocessorTests.cpp cover both injectDefines (which had no coverage) and injectSnippet edge cases. Build: Snake3 + Tests green, ctest 24/24 passing. Note: `file(GLOB Tests/*.cpp)` is evaluated at cmake-configure time, so after adding a new test file `cmake ..` must run before the test list picks it up. Builds without reconfigure silently skip new tests. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase B4 lands the material composition API that B5 will start migrating
existing materials onto. Nothing in the render path uses these classes
yet - StandardMaterial / PlaneMaterial keep their current bind() shape
until B5.
New files:
- Renderer/Opengl/Material/RenderContext.h - struct that bundles
viewPos/view/projection/model/uTime/shadows, replaces the 5-arg
StandardMaterial::bind signature.
- Renderer/Opengl/Material/TextureSlots.h - central enum of texture
units. Each feature reads its slot from here so collisions are caught
by the unique-values test instead of by visual debugging.
- Renderer/Opengl/Material/Feature/IMaterialFeature.h - abstract:
flag(), bind(shader, ctx), unbind(shader), clone().
- Renderer/Opengl/Material/Feature/HoleMapFeature.{h,cpp} - first
concrete feature, owns one TextureManager, binds slot 8, mirrors the
PlaneMaterial::bind hole-map block 1:1.
- Renderer/Opengl/Material/MaterialInstance.{h,cpp} - inherits
BaseMaterial. Composes program + features list. bind(ctx) issues
use(), writes the five core uniforms, then dispatches to each
feature. clone() shares program (immutable GL handle) and deep-copies
the feature objects (which themselves share textures).
- Renderer/Opengl/Material/MaterialBuilder.{h,cpp} - fluent API.
useMaster() / with() accumulate, build(registry) ORs feature->flag()
into a ShaderFeatureMask, asks the registry for that permutation,
and wraps the program in a MaterialInstance.
Six Catch2 cases in Tests/MaterialBuilderTests.cpp cover the GL-free
parts: HoleMapFeature flag + clone (texture handle sharing), builder
accumulation order, featureMask OR-folding, nullptr ignore, and a
compile-time-ish guard that TextureSlots values are pairwise unique.
Build: Snake3 + Tests green, ctest 30/30 passing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase B5a fills out the feature catalogue that B5c will use to rebuild the plane via MaterialBuilder. No materials are migrated yet - StandardMaterial / PlaneMaterial still own the render path. New features under Renderer/Opengl/Material/Feature/: - FogFeature: toggles fogEnable runtime. Flag: Fog. - UvTransformFeature: uvScale + uvOffset. No flag. - AlbedoFeature: albedo texture (slot 0) + optional color override + alpha + ambient intensity. Mirrors the "color half" of StandardMaterial::bind. No flag. - NormalMapFeature: tangent-space normal (slot 1). Flag: NormalMap. - SpecularFeature: specular highlight (slot 2) + shininess. No flag. - ShadowFeature: CSM array sampler2DArray (slot 3) + shadowDepth shader. Crosses with RenderContext::shadows global toggle. Flag: Shadows. - LightingFeature: directional + point + spot lights bundled. Flag is DirectionalLight only when a directional light is present - point / spot loops in basic.fs aren't gated, so adding their flags would fragment the permutation cache pointlessly. - PlanarReflectionFeature: reflection texture (slot 20) + clipPlane + runtime enable. No flag for now; B8 converts the reflection composite into a snippet. All concrete features inherit IMaterialFeature, store an explicit set of shared_ptr resources, deep-copy themselves in clone() (shared textures/lights, owned scalars). Each implementation mirrors the matching slice of StandardMaterial::bind 1:1 so the B5c migration becomes a behavior-preserving swap. Fifteen GL-free Catch2 cases in Tests/MaterialFeaturesTests.cpp cover flag() and clone() per feature, including clone-independence checks (mutating original after clone doesn't leak into the copy). Build: Snake3 + Tests green, ctest 45/45 passing. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase B5b plugs MaterialInstance into the existing render path. Each
draw now picks the binding strategy from the concrete material type:
1. MaterialInstance (new B4 path) -> build a RenderContext and call
instance->bind(ctx); features write their uniforms.
2. StandardMaterial (legacy) -> the existing 5-arg bind() call.
3. Anything else -> the raw-shader fallback that was already there.
unbind() mirrors the same dispatch so feature::unbind hooks run after
the draw.
uTime is sourced from glfwGetTime() at the call site; per-material
timers stay on StandardMaterial only. A single shared clock is cleaner
once MaterialInstance fully replaces StandardMaterial (B6+).
No materials currently are MaterialInstance, so the new branch is
unreachable at runtime - this PR is purely a wiring step. B5c will
flip the plane to use it.
renderShadowMap is untouched: only StandardMaterial casts shadows.
Plane is a flat receiver, so the upcoming B5c migration doesn't lose
anything. If a MaterialInstance-built mesh ever needs to write a
shadow depth pass, B6 adds an analogous dispatch keyed on
ShadowFeature::shadowDepthShader.
Build: Snake3 + Tests green, ctest 45/45 passing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase B5c flips the floor from the legacy PlaneMaterial hierarchy to a
MaterialInstance built with MaterialBuilder. First real consumer of B4
and B5a.
- MainScene.h: planeMaterial is now shared_ptr<MaterialInstance>; two
feature handles are kept (HoleMapFeature, PlanarReflectionFeature) so
runtime mutation paths (per-level hole map rebuild, F2 reflection
toggle) poke just the relevant feature instead of rebuilding the
whole material.
- MainScene::initPlane composes nine features through MaterialBuilder
(Lighting + Shadow + NormalMap + Specular + UvTransform + Fog +
Albedo + PlanarReflection + HoleMap) and asks the registry for the
matching basicShader permutation. PlaneMesh receives
planeMaterial->getProgram() directly.
- MainScene::applyHolesToPlane and the F2 GLFW handler write through
the feature handles.
- HoleMapFeature gains setTexture() so per-level applyHolesToPlane can
swap the underlying TextureManager without rebuilding the material.
- ResourceManager::{set,get}ShaderRegistry forwards the registry that
App owns to scenes; cleaner alternative to threading the registry
through every scene ctor.
- App drops the planeShader alias - nobody calls
getShader("planeShader") anymore; the builder asks the registry
directly for basicShader | HoleMap.
Files removed:
- Renderer/Opengl/Material/PlaneMaterial.{h,cpp}
- Renderer/Opengl/Material/PlanarReflectionMaterial.{h,cpp}
Both classes were only used by the plane and now have no callers.
Build: Snake3 + Tests green, ctest 45/45 passing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase B6a finishes the IMaterialFeature catalogue. With these three the builder can express every material the engine currently uses. - PbrFeature: metalness (slot 4) + roughness (slot 5) + aoMap (slot 7). Activates pbrEnabled at runtime so the Cook-Torrance branch in basic.fs runs. Flag: PBR. NOTE: preserves a pre-existing quirk in StandardMaterial::bind that sets `roughness` and `aoMap` as sampler names while basic.fs declares `roughnessMap` and `material.aoMap`. The mismatched setters no-op (location -1) so those samplers stay at default unit 0 and effectively read the albedo texture. We keep the same behaviour to avoid changing rendering during B6; a dedicated follow-up can normalise the names later. - IblFeature: environmentMap cubemap (slot 6). Drives the IBL diffuse + specular branches in basic.fs (FEATURE_IBL inside FEATURE_PBR). Compositionally separate from PbrFeature so a material can opt into PBR without IBL. - BonesFeature: useBones runtime toggle. Bone matrices themselves stay on the AnimationArrayMesh side, which pokes setMat4 directly per draw - that per-frame matrix stream doesn't fit the static feature shape. Flag: Bones. Six new Catch2 cases cover flag() and clone() / share-resources for each. No material is migrated yet; B6b starts on SnakeMeshNode3D. Build: Snake3 + Tests green, ctest 51/51 passing. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase B6b flips the snake body segments (tileMaterial) from
StandardMaterial to MaterialInstance composed via MaterialBuilder.
Head material (loaded from gltf), respawn / crash / headRespawn
materials (ShaderMaterial free-form) are intentionally untouched -
they fit different patterns and migrate later (B6c-d).
Design adjustments:
- LightingFeature::flag() now always returns DirectionalLight,
regardless of whether a directional light is bound at build time.
Without this, snake's pattern of "build the material at ctor, set
directional later via setDirectionalLight" would compile the shader
without the FEATURE_DIRECTIONAL_LIGHT block and dir lighting would
never run on tiles. Test "LightingFeature flag depends on..." is
rewritten as "always advertises DirectionalLight" with the new
expectation. Cost: a few KB of compiled shader code per permutation
that runtime-checks directionLightEnable.
- SnakeMeshNode3D: tileMaterial -> shared_ptr<MaterialInstance>;
tileLightingFeature handle retained so set{Directional,Spot,Point}
Lights propagate without rebuilding the material. Construction now
uses MaterialBuilder with LightingFeature + ShadowFeature +
AlbedoFeature(color=red).
- copyExplosionSourceMaterial helper extended to handle
MaterialInstance: looks for an AlbedoFeature, extracts albedo /
color, feeds them to the crash explosion shader the same way it
used to from StandardMaterial::getColor()/getAlbedo(). Without this
body segments would crash to a colourless explosion.
- createTileNode ternary needed an explicit base-class cast since
MaterialInstance and ShaderMaterial no longer share a common
shared_ptr type at the ?:.
RemoteSnakeScene / PlayerScene unchanged: those create the head
material (pacmanMesh) which stays StandardMaterial; B6c migrates the
loaders that produce it.
Build: Snake3 + Tests green, ctest 51/51 passing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…B6b follow-up)
Visual regression spotted after B5c: the plane looked fogged out in the
distance. Root cause is that the original StandardMaterial::bind
unconditionally writes shader.setBool("fogEnable", false) and the F
key toggle never reaches the shader (RenderManager::toggleFog only
flips a renderer-side flag that nothing reads back into a uniform), so
the floor effectively never had fog. B5c blindly mirrored the feature
list and added FogFeature(true), which writes fogEnable=true and
finally activates the FEATURE_FOG branch in basic.fs.
Fix: remove FogFeature from the plane builder chain. The plane goes
back to no fog, matching what was on screen before the migration. When
fog gets wired up properly (so the F toggle actually writes a uniform),
FogFeature will reappear here with a handle that scenes can mutate.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase B6c flips both box-material call sites from StandardMaterial to
MaterialInstance:
- BarriersScene::initBarriers: outer perimeter walls. Same feature set
as before (Lighting + NormalMap + Specular + Albedo) with the
ambient intensity tweak (0.1) routed through AlbedoFeature.
- LevelManager::createLevel: per-level interior brick walls. Adds
AlbedoFeature::setColor({1,1,1}) to mirror the historical
setColor white that originally activated overrideColorMesh - the
AlbedoFeature path keeps the behaviour identical (since color = white
multiplied with white albedo texture leaves output unchanged, but
preserves the overrideColorMesh = true uniform that legacy code set).
Neither call site set a shadow texture in the old code, so no
ShadowFeature here. Boxes act as shadow casters (rendered into the
depth pass via StandardMesh::renderShadowMap if their material is
StandardMaterial)... which they're no longer. This may regress shadow
casting from walls; if so a separate fix wires renderShadowMap to also
handle MaterialInstance + ShadowFeature::shadowDepthShader. Plane
shadows on the floor still work because plane has ShadowFeature.
Build: Snake3 + Tests green, ctest 51/51 passing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase B6d converts the three prop classes to MaterialInstance via MaterialBuilder. Each prop holds feature handles for textures that load lazily from gltf assets after init. - BarrelNode3D: material -> MaterialInstance, with AlbedoFeature + NormalMapFeature handles for the post-init lazy texture binding in update(). LightingFeature + ShadowFeature complete the composition. - StreetLampNode3D: three material instances (lamp body, glass shade, bulb) with full PBR + IBL on the body, PBR on the shade, and HDR emissive color on the bulb. Albedo / normal / pbr feature handles for each so update() can wire texture data once the gltf loader delivers it. mesh3 (bulb) keeps no shadow feature to match the old behaviour. - TorchScene::initTorch: torchMaterial -> MaterialInstance with Lighting (dir + spots) + NormalMap + Albedo. Feature setter additions to support lazy texture binding: - NormalMapFeature::setTexture - PbrFeature::setMetalness / setRoughness / setAoMap - IblFeature::setEnvironmentMap Build: Snake3 + Tests green, ctest 51/51 passing. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase B6e1 restores shadow casting for objects whose materials were migrated to MaterialInstance in B5c/B6b/B6c/B6d. Until now StandardMesh::renderShadowMap only handled StandardMaterial via dynamic_cast - any other material type silently skipped the shadow depth pass, so walls / barrels / lamp / torch / snake tiles stopped contributing to the shadow map. New surface: - ShadowFeature::bindShadow(model): activates the depth shader and writes the model matrix. Returns false if no shadowDepthShader was supplied to the feature. - MaterialInstance::bindShadow(model): walks features, delegates to the first ShadowFeature, returns its bool. No ShadowFeature -> not a caster, the mesh skips its shadow draw. - StandardMesh::renderShadowMap dispatches MaterialInstance first, then falls back to the legacy StandardMaterial path. Both branches issue the same glDrawElements after binding. Materials that already had a ShadowFeature (plane via B5c, snake tile via B6b, barrel + streetlamp via B6d) immediately resume casting. Materials without it (level boxes from B6c, torch from B6d, lamp bulb mesh3) continue not casting - that mirrors their pre-migration setup (no setShadow call) and is intentional. Build: Snake3 + Tests green, ctest 51/51 passing. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase B6e2 extends the skeletal-mesh renderer so it can drive a MaterialInstance-built material. Required before head materials can migrate in B6e3 - until then no real material hits the new branch, animation still flows through the legacy StandardMaterial path. - MaterialInstance::getShadowProgram returns the ShadowFeature's shadowDepthShader (or nullptr). renderMesh uses it to know which program to write per-mesh `model` and bone matrices into during the shadow pass. - AnimationArrayMesh::render gains a MaterialInstance branch before StandardMaterial. Bind builds a RenderContext (viewPos, view, projection, parentTransform, glfwGetTime, shadows) and delegates; unbind mirrors. - renderShadowMap gains the same MaterialInstance-first dispatch. bindShadow returns false when no ShadowFeature is present so a head without shadow casting silently skips the draw. - renderMesh resolves the active program once (main vs shadow) then writes finalBonesMatrices / useBones / model directly to it. StandardMaterial branch unchanged; bare-shader fallback unchanged. Build: Snake3 + Tests green, ctest 51/51 passing. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase B6e3 swaps the pacman head materials in PlayerScene::initSnake and RemoteSnakeScene::initSnake from StandardMaterial to MaterialInstance. With B6e2 in place, AnimationArrayMesh::render now flows through the MaterialInstance branch for skeletal head draws. Mirror of the previous setup: - LightingFeature: directional + point + spot lights (local DirectionalLight for the remote case) - ShadowFeature: depth array + shadowDepthShader - NormalMapFeature(nullptr): the head never explicitly received a normal texture - the legacy setNormalEnabled(true) call advertised the feature flag but with no texture bound, which `setBool` for normalMapEnabled left at runtime-true. AlbedoFeature(nullptr) keeps useMaterial=true so the gltf-baked vertex colors continue showing through. Snake's headMaterial member is `shared_ptr<BaseMaterial>`, so no type change required there - it just stores the MaterialInstance returned by builder. copyExplosionSourceMaterial already handles MaterialInstance (B6b) so the explosion effect picks up albedo / color from the head feature pack when the head is the explosion source. Build: Snake3 + Tests green, ctest 51/51 passing. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase B6e4 strips the remaining gameplay StandardMaterial usages: - CoinScene::initCoin: gold coin material with PBR (metalness + roughness) + IBL (skybox cubemap) + NormalMap + Albedo. Spotlights go through a LightingFeature handle, replacing the StandardMaterial::addSpotLight loop with LightingFeature::setSpots after build. - Physic shapes (Box / Sphere / Capsule / Cylinder): the debug-only translucent collision visualizers now build a MaterialInstance with a single AlbedoFeature (alpha=0.2). The per-frame red/cyan colour toggle goes through albedoFeature->setColor instead of the legacy StandardMaterial::setColor. A null-resourceManager guard is added for the test harness, which instantiates shapes with no managers. After B6e4 the only remaining StandardMaterial referrer is ShaderMaterial (which inherits StandardMaterial for the lights / textures fields used by snake respawn / crash / ring / bolt / corner custom shaders). Decoupling ShaderMaterial is a B7 cleanup task - StandardMaterial itself will live on until that lands. Build: Snake3 + Tests green, ctest 51/51 passing. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Snake head rendered as half a skull with no animation after B6e3. Root cause: head material composition didn't include BonesFeature, so its feature mask resolved to DirectionalLight | Shadows | NormalMap, and the registry compiled basic.vs without FEATURE_BONES. basic.vs gates the bone transform branch with `#ifdef FEATURE_BONES`, with a parallel `if (!useBones)` static fallback. Without the define the bone branch is stripped. AnimationArrayMesh::renderMesh still writes useBones=true for skeletal sub-meshes, which then turns `if (!useBones)` false too - gl_Position is never written and vertices end up at garbage positions. Fix: add BonesFeature to head composition in PlayerScene::initSnake and RemoteSnakeScene::initSnake. The feature flag advertises Bones, shader compiles with FEATURE_BONES, and the per-mesh useBones toggle from AnimationArrayMesh works as intended. No other migrated materials need this: plane / level walls / props / coin / snake body tiles / physics visualizers are all non-skeletal and run useBones=false through the always-available static fallback. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase B7a strips the StandardMaterial inheritance from ShaderMaterial. The two classes no longer share a base beyond BaseMaterial, paving the way for B7b to delete StandardMaterial entirely. What moved into ShaderMaterial as direct members: - shader / shadowDepthShader (the two ShaderManager handles). - albedo / normal / specular / metalness / roughness / shadow texture shared_ptrs and their setters/getters - that's the field surface SnakeMeshNode3D::copyExplosionSourceMaterial reads from the crash material. - DirectionalLight / SpotLight / PointLight vectors and setters, matching how snake's setDirectionalLight propagates lights into the respawn/crash materials. - color + alpha plus setBlending (which already comes from BaseMaterial's BlendingInterface) - no behavior change. - New methods: unbind() (legacy texture-slot cleanup is a no-op now since ShaderMaterial doesn't manage well-known slots), bindShadow() (mirrors StandardMaterial::bindShadow), isShadowEnabled() and getShader / getShadowDepthShader accessors used by AnimationArrayMesh. Mesh dispatch sites pick up a dedicated ShaderMaterial branch: - StandardMesh::render and renderShadowMap try MaterialInstance first, then ShaderMaterial, then legacy StandardMaterial, then raw-shader fallback. Same for unbind. - AnimationArrayMesh::render and renderShadowMap mirror the same chain for skeletal draws. - AnimationArrayMesh::renderMesh resolves the active program for the current pass via MaterialInstance::getProgram or ShaderMaterial::getShader (and the shadow variants), then writes bone matrices / model / useBones to whichever shader the material declares. Legacy StandardMaterial fork untouched. ShaderMaterial::clone now deep-copies its own state (color cloned, shared_ptr fields aliased for textures/lights as before) instead of relying on inherited copy. Build: Snake3 + Tests green, ctest 51/51 passing. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
After B7a's ShaderMaterial decouple, the three 2D render sites still only knew about StandardMaterial via dynamic_pointer_cast. ShaderMaterial instances (menu buttons / corner shaders / text labels / image fx) silently fell into the else branch and were drawn against the wrong baseShader, producing a fully black scene with only the bolt flash visible. Fix: BaseNode2D, ImageNode2D and LabelNode2D each try ShaderMaterial first, then StandardMaterial, then fall back to baseShader. unbind paths in BaseNode2D and ImageNode2D mirror the same chain. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase B7b removes StandardMaterial and IAlbedoMaterial. Everything in
the gameplay path renders through MaterialInstance now (B5-B6) and the
free-form custom shaders go through ShaderMaterial (B7a), so nothing
left referenced StandardMaterial except the dead dispatch fallbacks.
What went:
- Renderer/Opengl/Material/StandardMaterial.{h,cpp}
- Renderer/Opengl/Material/IAlbedoMaterial.h (only StandardMaterial
implemented it; no other use)
- Three CMakeLists entries (executable, APP_SOURCES, snake3d_lib).
- Every `else if (StandardMaterial)` fallback branch in
StandardMesh::render/renderShadowMap/unbind,
AnimationArrayMesh::render/renderShadowMap and the three
per-bone-matrix/useBones/model writes in renderMesh, and in
BaseNode2D / ImageNode2D / LabelNode2D 2D dispatchers.
- SnakeMeshNode3D::copyExplosionSourceMaterial loses its
StandardMaterial fallback - the head is a MaterialInstance after
B6e3, so the MaterialInstance feature-pack walk is the only path
the explosion shader needs.
Transitive include casualties (StandardMaterial.h used to pull these
in for callers): explicit `#include` added in
- Renderer/Opengl/Model/Standard/MeshNode3D.h: DirectionalLight,
PointLight, SpotLight + `using namespace Lights` so derived classes
like CoinMeshNode3D / SnakeMeshNode3D compile.
- Renderer/Opengl/Scene/Scene.h: Tools/Environment.h.
- App.h: Tools/Environment.h.
- StandardMesh.h swaps StandardMaterial.h for BaseMaterial.h since
StandardMesh only needs BaseMaterial polymorphism now.
Build: Snake3 + Tests green, ctest 51/51 passing. No gameplay change
- the deleted branches were unreachable after B7a.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…rRegistry (B8a) Phase B8a wires snippet injection end-to-end through the build pipeline. No real feature uses it yet - B8b's PlanarReflection conversion is the first live consumer. - ShaderHandle.snippets (marker -> .glsl path) joins master + features in the cache key. ShaderHandle::operator== compares snippets too, and makeKey hashes them deterministically by relying on std::map's ordered iteration. - IMaterialFeature::snippetPaths() virtual returns an empty map by default - existing features keep behaving via `#ifdef FEATURE_X`, only features that need code-injection override. - MaterialBuilder::build collects snippets from every feature in the composition before asking the registry for the program. - ShaderRegistry::get loads each snippet's source (with `#include` resolution) and calls ShaderPreprocessor::injectSnippet on the vertex, fragment and geometry sources. Markers that don't appear in a given stage are silently skipped, so per-stage snippets fall out for free. Three Catch2 cases lock the new surface: - IMaterialFeature default snippetPaths is empty. - MaterialBuilder picks up declared snippets from a fake feature. - ShaderHandle equality distinguishes by snippets. Build: Snake3 + Tests green, ctest 54/54 passing. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase B8b is the first live use of the B8a snippet pipeline. The
planar reflection block leaves basic.fs and lives in its own .glsl
file, injected by PlanarReflectionFeature.
- New Assets/Shaders/snippets/planar_reflection.glsl carries the
if (reflectionEnable) blob that used to sit near the bottom of
main(). Comments document the in-scope symbols the snippet expects
(clipSpacePos, FragColor, calcReflexion from reflection.glsl,
rainDropEnable + rippleOffset still on the basic.fs side).
- basic.fs replaces the blob with `// @MATERIAL_FRAGMENT_POST` marker.
When a material doesn't add PlanarReflectionFeature, the marker
stays as a plain comment and the program runs without the
reflection composite.
- PlanarReflectionFeature::snippetPaths returns
{@MATERIAL_FRAGMENT_POST -> snippets/planar_reflection.glsl}.
ShaderRegistry's cache key now treats reflective and non-reflective
permutations as distinct programs (cheaper than runtime branching
and feeds nicely into the bigger story of C phase named placeholders).
Visual result: plane's reflection (the only consumer of the feature
today) keeps working through F2 toggle - PlanarReflectionFeature::bind
still flips reflectionEnable at runtime. Other materials never had
the feature in their builder chain, so they're untouched.
Build: Snake3 + Tests green, ctest 54/54 passing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Final B8 step: the rain ripple distortion in basic.fs migrates from the runtime `if (rainDropEnable)` pattern to the regular feature-flag pattern used by everything else (PBR, NormalMap, Shadows...). Why feature flag instead of snippet: the ripple touches three sites in main() (compute rippleOffset, perturb tangent normal, derive normal from ripple when no normal map). Splitting that across snippet placeholders would need 2-3 markers, all interleaved with feature blocks. A flag is simpler and consistent with the rest of the gate-by-#ifdef family. - ShaderFeature::RainRipple (bit 12) + FEATURE_RAIN_RIPPLE define. - basic.fs wraps the three rainDropEnable blocks in `#ifdef FEATURE_RAIN_RIPPLE`. The rippleOffset local declaration stays unconditional because the planar reflection snippet from B8b reads it - without the feature it just stays vec2(0.0) and the snippet's `if (rainDropEnable)` skips the distortion at runtime (rainDropEnable is still a uniform that defaults to false). - New RainRippleFeature header-only class follows the FogFeature pattern: scalar params (enabled, speed, density) plus flag() and bind(). Dormant in this commit - no material adds it to its composition. Future weather work can drop it into the plane chain to bring the effect online. No gameplay change today: `rainDropEnable` was never wired from C++ side, so the gated code was already dead. The shader is now smaller for materials that don't request the feature. Build: Snake3 + Tests green, ctest 54/54 passing. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
DepthMapRenderer::render used to write lightSpaceMatrix0/1/2, cascadeEnds0/1/2 and shadowCenter only on ResourceManager's "basicShader" entry. After the B refactor each material gets its own program permutation (basicShader|features, basicShader|features|HoleMap, basicShader|features|Bones, ...), so the plane / snake / props all held their own program IDs and the per-frame matrices never landed on them. Shadow sampling against an uninitialised matrix0..2 produced the no-shadows-on-plane regression the user reported. Fix: iterate ShaderRegistry::cachedPrograms() and write the shadow uniforms on every cached program. Programs that don't have those uniforms (particle update, 2D, skybox, ...) silently no-op since glGetUniformLocation returns -1. Performance is fine: ~20 cached programs * 7 setX calls per shadow render is negligible. ShaderRegistry exposes cachedPrograms() as a read-only getter for this kind of broadcast use case. Hot reload (later C-phase) will probably clear the cache through clearCache() and re-emit the broadcast on next frame. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
PBR materials (coin, street-lamp post) were getting Phong-style spotlight contribution. CalcSpotLight reads material.shininess, which PbrFeature doesn't set - it defaults to 0, and pow(specAngle, 0) = 1 across the whole hemisphere, so highlights spread into bright smudges under street lamp / torch spots. - lights.glsl gets a proper CalcSpotLightPBR implementation: same Cook-Torrance kernel as CalcPointLightPBR plus the spot cone falloff and pulse modulation lifted from CalcSpotLight. Lives under #ifdef FEATURE_PBR like the other PBR helpers (B3c). The old commented stub is gone, the prototype updated to the new signature. - basic.fs branches the spotlight loop on pbrEnabled, mirroring how point lights were already split. `normal`, `ambient`, `roughness`, `metalness`, `F0` are all in main() scope from earlier in the function, so the PBR loop just plumbs them into the new helper. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CoinScene used AlbedoFeature's default ambientIntensity (0.05), which is fine for diffuse materials but starves a metallic + IBL surface. With metalness ~= 1 the diffuse path goes to zero, leaving the coin's ambient lit only by the cubemap reflection at R = reflect(-viewDir, normal). As the coin spins, R sweeps the skybox and lands on dark faces (cloud-skybox bottom), making the coin briefly go black before the proper spot/dir contribution kicks back in. Setting AlbedoFeature::setAmbientIntensity(2.0f) gives the reflection a baseline brightness multiplier so the coin stays readable across the rotation. 2.0 lands between the default 0.05 and the 2.5 the street lamp post uses; visually balanced per playtest. This was masked before c77d2a8: the Phong CalcSpotLight running on a PBR material was bogus-bright (material.shininess defaulted to 0 so pow(spec, 0)=1 everywhere) and made the coin look brighter than it should. The PBR-correct spot light exposed the underlying ambient shortfall. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase B7c: rename Manager::ShaderManager to Manager::ShaderProgram
across the codebase. The class wraps a GL program handle; "Program"
is the standard GL terminology and avoids the confusion with the
other *Manager helpers (ResourceManager, RenderManager,
KeyboardManager, ...).
What moved:
- Manager/ShaderManager.{h,cpp} -> Manager/ShaderProgram.{h,cpp}
(renamed via git mv so blame survives).
- Header guard SNAKE3_SHADERMANAGER_H -> SNAKE3_SHADERPROGRAM_H.
- Class declaration and every member-function definition.
- Every callsite across the engine (~102 files): includes, type
names, shared_ptr<ShaderProgram>, etc. Driven by a single
word-boundary sed pass; only `ShaderManager` appears in the
codebase (no aliases, no near-matches), so the global replace is
safe.
- CMakeLists.txt entries (executable inline list, APP_SOURCES,
snake3d_lib reuses APP_SOURCES so only two edits in practice).
Mechanical rename only - no behavior change. Build passes, ctest
54/54 still green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
H1-H4a phase work and performance optimization (FPS 11 -> 44): Engine/game split: - CMakeLists.txt refactored into three targets: snake3d_engine (static lib), snake3d_game (static lib, Snake3-specific scenes/nodes/netcode), Snake3 (executable shell). Tests links engine+game. - Game code physically moved under examples/snake3/src/ (Scenes, game-side Renderer/Opengl/Model/Game, Network/Game, LevelManager/EatManager, SnakeMove/EatLocation/Radar handlers, App, main). Assets moved to examples/snake3/Assets. - Bulk include rewrite via basename lookup; engine paths become root-relative. - Removed dead engine->game couplings (TextureLoader.h pulling EatManager, KeyboardManager.h pulling SnakeMoveHandler). - IMaterialFeature.h: removed top-level using-namespace pollution. Performance (FPS 11 -> 44): - ShaderProgram uniform location cache (no per-set glGetUniformLocation). - CollisionSystem3D: cached enable/layer/mask in entry, static/dynamic pair split (3M -> 24k pairs/frame). - Static collider reclassification in BarriersScene/TorchScene (140 perimeter walls + barrel + 4 torches flipped from dynamic to static). - MeshNode3D transform-dirty short-circuit in computeWorldMatrix (2300+ floor cells skip recompute). Dev tools: - D4 pre-flight: ShaderRegistry::warmupAll compiles base permutations at bootstrap, fatal abort on failure. - ImGui Object Inspector: unified select across Position/Scale/Rotation/ Collision/Lights handlers; per-type fieldset (transform always, light colors + direction, collision layer/mask); Focus camera + Print buttons; default position no longer overlaps Engine panel. - ImGui Shader Inspector (POC): list cached programs, watched paths tooltip, per-program Reload, force F10 hot reload. - RenderStats with per-frame draw counter (per-pass split), main pass CPU/GPU timing, update/physics/render/swap phase timing, physics breakdown (aabb/pairs/colliders). - Camera: focusOn(target) one-shot teleport without sticky bind; mouse rotation gated to Ctrl/RMB in debug so ImGui sliders are usable. CI: - .github/workflows/ci.yml: Linux Ubuntu Debug + Release matrix. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.