Feature/physics#47
Open
ViPErCZ wants to merge 89 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>
Five 2D node headers cleaned, completing the 2D family started in the prior leaf-headers batch (8167dc9). Cleaned (mix of narrow + inline qualification): - GPUParticle2D.h: drop std. - LabelNode2D.h: drop std + Material. - ImageNode2D.h: drop std + Manager. - MeshNode2D.h: drop std + Manager. - BaseNode2D.h: drop std + ModelUtils + Manager + Material. Used narrow `using std::shared_ptr;` (and `std::string` in LabelNode2D.h) in headers where the symbol appeared 4+ times -- first H4b iteration to use that allowance (prior commits stayed inline-only). Downstream impl files got `using namespace X;` added: BaseNode2D.cpp, TringleNode2D.cpp, QuadNode2D.cpp, ImageNode2D.cpp, LabelNode2D.cpp, GPUParticle2D.cpp (Tools:: imports from the prior leaf batch retained). No header cascade outside the five targets. Tools:: qualifications from the prior commit (Tools::Blending, Tools::ContextState) stayed intact. Build green for Snake3 + Tests. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Continuation of the Renderer/Opengl/ scope. Seven small-subdir headers
cleaned across Model/Collision/, Model/, Material/Particle/,
Material/Feature/, and Model/Standard/Animation/.
Cleaned:
- SpinnerMesh.h: drop ModelUtils (was redundant -- symbols qualified
elsewhere).
- CollisionShape3D.h: drop Physic + Model. **Major latent leak** --
Scene.h + 5 game-side .cpp files compiled only because Physic was
transitively injected; 4 game-side headers needed Physic::/Model::
inline qualification too. All under park threshold.
- BaseProcessMaterial.h: drop std; surfaced latent unqualified
Manager::ShaderProgram / ResourceManager that worked only via TU-level
leaks elsewhere -- fixed inline.
- ParticleProcessMaterial.h: drop Manager + std.
- UvTransformFeature.h: drop Manager + Material.
- RainRippleFeature.h: drop Material + Manager + std.
- AnimationPlayer.h (hub): drop std + ModelUtils. 12 std symbols
qualified via narrow `using std::{shared_ptr,vector,string,map,
unordered_map};` (4+ refs each). Forced ResourceManager.h to add
narrow `using ModelUtils::Mesh;` (6 refs) and AnimLoader.h to inline-
qualify ModelUtils::Mesh/Tree.
Downstream `.cpp` files received `using namespace X;` after includes
(12 total) -- mostly Physic + ModelUtils chains from CollisionShape3D
and AnimationPlayer leaks.
Build green, 70/70 tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three Material/ root headers cleaned: BaseMaterial.h, IUniform.h, ShaderMaterial.h. No header cascade beyond a 1-line touch. Cleaned: - BaseMaterial.h: drop std (1x std::shared_ptr in clone()). - IUniform.h: drop std + Manager (4 sites total). Added #include <string>. - ShaderMaterial.h: drop Manager + Lights + std. Used narrow `using Manager::ShaderProgram;` + `using Manager::TextureManager;` inside namespace Material for heavily-referenced symbols. Downstream impl files received `using namespace X;` added: - ShaderMaterial.cpp (std) - FadeInUniform.cpp (std + Manager) - CallbackUniform.cpp (Manager) FadeInUniform.h was a stragler from the Material/Uniform batch (8167dc9) -- inline-qualified one signature (std::shared_ptr< Manager::ShaderProgram>, std::string). Build green for Snake3 + Tests. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Six of eight renderer headers cleaned. Removed 14 `using namespace` directives. Cleaned: - DepthMapRenderer.h: drop Manager + Lights. - BloomRenderer.h: drop Manager + std. - PlanarReflectionRenderer.h: drop std + Manager. - Node3DRenderer.h: drop Model + std. - Node2DRenderer.h: drop Manager + Model + std. - SceneRenderer.h: drop std + Renderer + Model + Manager. Downstream impl files received `using namespace X;` after includes (7 .cpp files + narrow `using Renderer::RenderStats;` in MainScene.cpp to avoid pulling all of Renderer:: namespace into its 2000-line body). Parked: - Scene.h. Has `using namespace std/Model/Manager/Handler::Debug/Build`. Cascade analysis identified ~15-16 game-scene headers under examples/snake3/src/Scenes/ that rely on these transitive imports (shared_ptr<DirectionalLight>, shared_ptr<RenderManager>, shared_ptr<MeshNode3D>, isDebug constant, etc.). Needs a coordinated multi-header batch follow-up. Notable gotchas: - ContextState lives in Tools::, not Manager:: (recurring confusion between transitive leak source and real namespace home). - RenderStats lives in Renderer:: -- MainScene.cpp was using it via SceneRenderer.h's leak. Build green for Snake3 + Tests. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Major Renderer/Opengl/ progress. Cleaned 9 of 10 Model/Standard/ headers + 4 sibling Standard headers + 2 game-side mesh headers via inline cascade. MeshNode3D.h re-parked. Cleaned (Model/Standard/): - BoxMesh.h, CylinderMesh.h, ArrayMesh.h, AnimationArrayMesh.h: drop std. - SkyboxNode3D.h: drop Uniform (unused) + dropped unused FadeInUniform include. - GPUParticle3D.h: drop Manager. - PlaneMesh.h / SphereMesh.h: drop Manager + ModelUtils + std. - StandardMesh.h (HUB): drop ModelUtils + Material + Tools + std. StandardMesh.h cleanup cascaded into 4 sibling Standard headers (WireframeArrowMesh, CapsuleMesh, QuadMesh3D, TringleMesh3D) and 2 game headers (SnakeMeshNode3D, MarkRingNode3D) -- all inline-qualified in place under the 5-header park threshold. Downstream impl files received `using namespace X;` after includes (20 .cpp files: all 11 Model/Standard/*.cpp + LightsHandler.cpp + 4 game scenes + 4 game mesh nodes). Re-parked: - MeshNode3D.h. Cleanup cascades to 5 headers (CollisionShape3D, DirectionalLightNode3D, GPUParticle3D, SkyboxNode3D, Scene.h consumers). - Vbo.h (still). `using namespace Manager;` is load-bearing for Camera/ResourceManager/ShaderProgram propagation across Mesh2D, MeshNode3D, PlaneMesh, CollisionShape3D, DirectionalLightNode3D, LightNode3D (6 headers). Three parked headers remain in Renderer/Opengl/: Vbo.h, MeshNode3D.h, Scene.h. They form a mutually-load-bearing cluster -- next iteration needs a coordinated combined batch + their ~10-15 downstream consumers (Physic/, Handler/Debug/, game-side mesh nodes + scenes). Build green for Snake3 + Tests. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace the "H4b TODO" stub with the actual progress so far: - 12 subdirs cleaned across two-thirds of the engine. - Animation namespace rename + unification noted. - Remaining: 3 parked headers (Vbo.h, MeshNode3D.h, Scene.h) forming a mutually-load-bearing cluster, plus ~16 game-scene headers that would cascade with Scene.h. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The D4 transform-dirty short-circuit returned early when this node's local + parent transform were both clean. That assumed children also stayed clean, which is wrong: ImGui Object Inspector can mutate a child's local transform (e.g. CollisionShape3D position/rotation/scale on an unchanged barrel parent) and the child relies on its own computeWorldMatrix to update worldMatrixCache. With the short-circuit, the parent never recursed, the child's cache stayed stale, and the shape rendered at its pre-edit position regardless of inspector input. Fix: split the short-circuit -- still skip the self recompute when self + parent are clean, but always recurse into children so child- driven dirty bits propagate. Cost: ~one cheap function call + matrix compare per child per frame (2300+ floor collision cells in snake3 level become 2300 fast no-op recursions instead of 0). Negligible at the scale we're at; the correct visual update wins. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
focusOn read target->getModelMatrix(), which is the LOCAL transform. For a top-level node (root barrel) local == world so the bug was invisible, but a CollisionShape3D child of barrel returned only its local offset relative to the parent -- camera teleported to local origin instead of the actual world position. Same issue in the right-click sticky resnap path in onMouseDown. Added a virtual `getWorldMatrix()` on Node3D::Transform with the plain-Transform fallback returning getModelMatrix(). MeshNode3D overrides to return its worldMatrixCache (already maintained by the scene-graph computeWorldMatrix pass). Camera::focusOn and the sticky resnap branch now both call getWorldMatrix. Pairs with the prior commit's child-recursion fix: together they make ImGui Inspector edits propagate to (a) the rendered shape position and (b) the focus-on jump. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When the ImGui Object Inspector has a CollisionShape3D selected whose owner has a registered DynamicBody (e.g. the snake head), pause that body. Without this, the user scaling a shape via the inspector caused resolveTopContact to "snap" the head upward (because the expanded AABB penetrated the floor), which is correct game-physics behavior but the wrong UX during a debug edit. Implementation: - CollisionSystem3D::findDynamicBody(node) -- public lookup added. - ImGuiOverlay holds a weak_ptr<DynamicBody> + prior-enabled flag. Each frame, computes the new pause target (collision shape's parent → its DynamicBody if any, walking one more hop up the parent chain for nested mesh groups). If the target differs from what's already paused, restores the previous body's prior state and pauses the new one. - MeshNode3D::setParent(...) added (public) so SnakeMeshNode3D::set- CollisionShape can wire the shape's parent (it stored shapes in a separate vector, not via addNode, so getParent() returned null -- silently broke this feature plus any other code that walked the parent chain from a snake collision shape). - ImGuiOverlay::setCollisionSystem(...) setter wired in App.cpp via MainScene::getCollisionSystem() (avoided touching Scene.h since it's parked in the H4b cluster). - ImGui combobox gained an explicit "(none)" entry at top so the user can deselect and resume physics without picking another object. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous inspector auto-pause toggled DynamicBody::setEnabled(false), but SnakeMoveHandler::update calls dynamicBody->setEnabled(true) every frame while the snake is alive and not chain-falling. The result: pause held for one frame, gravity re-applied the next, and the head visibly fell through the floor while the user was editing its shape. Added a separate gate: DynamicBody::setDebugFrozen + DynamicBody::isActive (which checks both enabled AND !debugFrozen). CollisionSystem3D::step phases 1/2/4 now consult isActive(). ImGuiOverlay touches only the debugFrozen flag, so gameplay's enabled toggling can't undo a UI pause. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Engine -> Shader Inspector (default collapsed) -> Object Inspector, all anchored at x=10 with each panel's y = previous panel's bottom + 6px gap. SetNextWindowPos uses ImGuiCond_Always + ImGuiWindowFlags_NoMove so a dynamically-changing panel height (e.g. Shader Inspector expanding when uncollapsed) shifts everything below it without overlap. Removed the previous hand-tuned positions (540,10 for Shader, 10/380 for Object) which broke whenever a panel's height changed between sessions. Trade-off: panels are no longer drag-movable. The user explicitly preferred reliable stacking over manual placement. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
All three panels (Engine, Shader Inspector, Object Inspector) now share the same kPanelWidth (480 px) and each gets a max height of one third of the current display height. SetNextWindowSizeConstraints + NoResize flags mean ImGui shows a scrollbar inside the panel instead of letting the panel grow past its allotted slice and overlap the next stacked panel. Result: three expanded panels always fit on screen, and expanding/collapsing the Shader Inspector doesn't push Object Inspector off the bottom edge. Engine panel cleanups: - Dropped the "V = shadows, B = bloom (no getter)" caption. - Added Shadows (V) + Bloom (B) checkboxes (paired with new RenderManager::isShadowsEnabled / isBloomEnabled accessors so the checkboxes show real state). - Dropped the "Reload shaders (F10)" button -- duplicated the Shader Inspector's own button, and F10 still works directly. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Was capped at 1/3 of display height like the other two panels. Now its max height is computed dynamically as `displayHeight - stackCursorY - bottomMargin`, so it consumes everything left below Engine + Shader Inspector. When Shader Inspector is collapsed, Object Inspector grows to nearly the full column; when Shader Inspector expands, Object Inspector shrinks but still gets the leftover slice + scrollbar. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Step D3.1-D3.4 of the JSON material config plan (see .claude/tasks/D3.md).
Engine additions:
- Resource/MaterialLoader.{h,cpp}: loadFromFile/loadFromJson(path, rm)
returning MaterialSpec { MaterialBuilder builder; bool hasLighting/
hasShadow/hasFog; Tools::Blending blending }. Static features
(albedo/normalMap/specular/pbr/uvTransform/bones/ibl) are constructed
by the loader from JSON params; runtime-wired features (lighting/
shadow/fog) set their flag and rely on the callsite to inject live
C++ objects after load. Unknown feature types emit a cerr warning
and are skipped. Top-level "_comment" keys are silently ignored
(used in pilot JSONs to document the feature-order constraint --
shader permutation hash depends on insertion order).
- Manager/ResourceManager: thin loadMaterial(path) wrapper so callsites
don't include nlohmann/json directly.
- CMakeLists.txt: Resource/MaterialLoader.{h,cpp} added to ENGINE_SOURCES.
Pilot JSON specs (examples/snake3/Assets/Materials/):
- snake_tile.json: lighting + shadow + albedo {color:[0.88,0.05,0.05]} + fog
- barrel.json: lighting + shadow + normalMap (nullptr texture) + albedo
{ambientIntensity: 2.5, nullptr texture} + fog. Empty texture fields
preserve the BarrelNode3D::update lazy-load polling pattern.
- torch.json: lighting + shadow + normalMap {texture: "torch_normal.png"}
+ albedo {texture: "torch.png"} + fog.
Build + Tests green, 70/70 pass. No pilot migrations yet (D3.5-D3.7
reserved for next dispatch).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Steps D3.5-D3.7 of the JSON material config plan (.claude/tasks/D3.md). Foundation landed in b0f21ae; this commit swaps three pilot callsites from inline MaterialBuilder chains to resourceManager->loadMaterial() + runtime wiring of lighting/shadow/fog from live objects. Migrated: - SnakeMeshNode3D.cpp: tile material now loaded from snake_tile.json. Albedo color (0.88,0.05,0.05) lives in JSON. tileLightingFeature member pointer retained for setDirectionalLight propagation. - BarrelNode3D.cpp: barrel material from barrel.json. albedoFeature and normalFeature member pointers extracted via featuresView() loop + dynamic_pointer_cast so the lazy-async-texture polling in BarrelNode3D::update keeps working. - TorchScene.cpp::initTorch: torch material from torch.json. ShaderRegistry::makeKey is order-INSENSITIVE for features (OR-combined ShaderFeatureMask, not insertion-ordered hash), so JSON feature order doesn't affect the shader permutation cache key. The "_comment" preservation hint in the JSON files is currently more conservative than strictly necessary -- left as-is. No JSON files modified, no header changes, no new feature accessors needed (MaterialBuilder::featuresView already existed). Build + Tests green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Final D3 steps: 19 new Catch2 test cases for MaterialLoader + CLAUDE.md section documenting the JSON material pipeline. Tests/MaterialLoaderTests.cpp covers: - Empty + runtime-only feature lists. - Static feature mapping for albedo (color/alpha/ambientIntensity), normalMap, specular (with default-shininess contract), pbr (3 textures), uvTransform (vec2 scale + offset), bones (useBones flag), ibl. - Unknown feature type emits cerr + is skipped without crash. - `_comment` top-level key silently ignored. - Blending values: opaque / translucent / additive / alphaAdditive / modulate / text (5 SECTIONs). - Missing master defensive behaviour. - Texture resolution via stubbed Manager::ResourceManager (TextureManager is GL-free until addTexture(id)). All 89 ctest cases pass (70 pre-D3 + 19 new). No GL context, no file I/O. CLAUDE.md gains a "Data-driven materials (D3)" subsection: loader API, static vs runtime-wired feature catalog, standard wiring pattern, schema example, 3-step recipe for new materials, and explicit out-of-scope list (hot-reload, GUI editor, complex runtime-state materials). JSON `_comment` strings in all three pilot files clarified — feature order does NOT affect shader cache (ShaderRegistry::makeKey is bitmask-based, not insertion-ordered). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 1 of the final H4b iteration. 13 game-side headers in examples/snake3/src/Scenes/ and examples/snake3/src/Renderer/Opengl/ Model/Game/ no longer leak `using namespace X;` into consumers. Cleaned (32 directives removed total): - WeatherScene.h, CoinScene.h, RemoteSnakeScene.h, SceneLightFactory.h (single-namespace each). - PlayerScene.h, TorchScene.h (2 ns each). - WinnerScene.h, MainMenuScene.h, SceneHud.h (3 ns each). - BoltScene.h, MainScene.h, MarkRingNode3D.h, SnakeMeshNode3D.h (4 ns each). Standard pattern: inline qualification in headers (Model::MeshNode3D, std::shared_ptr, Tools::Timer, Physic::CollisionSystem3D, etc.), `using namespace X;` added after includes in 10 matching .cpp files where the impl bodies were previously relying on the leak. One game-level header outside cleanup scope (App.h) gained `using namespace Scenes;` to preserve its existing parked-style block. Build green, 89/89 tests pass. Still parked (will require a coordinated cluster cleanup with their ~14 transitive downstream headers): Vbo.h, MeshNode3D.h, Scene.h. Attempting any one in isolation cascades into 10+ headers each -- the three are mutually load-bearing for `Manager::ResourceManager`, `Lights::DirectionalLight`, `Handler::Debug::ManipulatorHandler`, `Build::isDebug`, and `std::*` across the entire engine + game scene graph. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Coordinated cleanup of the three previously-parked engine headers together with their entire downstream cascade. Removes the final `using namespace X;` directives from all engine-side headers. Cleaned (10 directives gone): - Renderer/Opengl/Model/Utils/Vbo.h: drop Manager + std. - Renderer/Opengl/Model/Standard/MeshNode3D.h: drop Tools + Lights + std. - Renderer/Opengl/Scene/Scene.h: drop std + Model + Manager + Handler::Debug + Build. The cluster was mutually load-bearing -- cleaning any one in isolation cascaded into 5-15 downstream headers. Doing all three plus their cascade as one big batch lets the iteration converge: - Cascade engine headers inline-qualified: Mesh2D.h, BoxMesh.h, CylinderMesh.h, PlaneMesh.h, SphereMesh.h, AnimationArrayMesh.h, GPUParticle3D.h, SkyboxNode3D.h, QuadNode2D.h, TringleNode2D.h, CollisionShape3D.h, LightNode3D.h, DirectionalLightNode3D.h, SpinnerMesh.h, CollisionSystem3D.h, plus all Handler/Debug/*.h. - Cascade game-side headers inline-qualified: all examples/snake3 game meshes (Barrel, BoltLines, Coin, MarkRing, Radar*, Snake, StreetLamp) + remaining scenes (Barriers, OrbitSceneBase, PreloaderScene, Preloader2Scene). - ~30 impl files received `using namespace X;` after includes across engine + game layers (per project convention -- impl bodies stay terse with using-directives, only headers are forbidden to leak). Build error count converged: 806 -> 421 -> 317 -> 261 -> 240 -> 131 -> 26 -> 0. Tests: 89/89 pass. H4b finish line for engine: a final grep across `Renderer/`, `Manager/`, `Physic/`, `Tools/`, `Lights/`, `Resource/`, `Handler/`, `Network/` headers returns ZERO `using namespace` directives. Remaining matches all live in `examples/snake3/src/` parked headers (App.h, EatLocationHandler.h, SnakeMoveHandler.h, RadarHandler.h, LevelManager.h, EatManager.h) which are game-layer concerns outside the engine boundary. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Engine-side `using namespace` audit completed. Last cluster (Vbo+MeshNode3D+Scene) landed in f6faa3f. Game-side parked headers explicitly listed as out-of-engine-scope. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
First batch of the engine canonical include path move (.claude/tasks/H4c.md). This commit lays the foundation: - git tag pre-H4c at prior HEAD (16000fc) for rollback. - Created the engine/include/snake3d/ + engine/src/ directory tree (40 subdirs total, mirroring the current root-level engine layout). - Moved stdafx.h to engine/include/stdafx.h. - Eight consumer files (7 engine headers + main.cpp) now `#include <stdafx.h>` instead of relative `"../stdafx.h"` / `"../../stdafx.h"`. - CMakeLists.txt: added `engine/include` as a second PUBLIC include directory for snake3d_engine (transitional dual-include path; the root entry is removed in the final batch once all engine files have moved). Build green for snake3d_engine + snake3d_game + Tests. ctest 89/89. Remaining batches: Lights/, Tools/, Resource/, Network/, Physic/, Handler/, Manager/, Renderer/ (in sub-batches), then CMakeLists final update + game-side + Tests include rewrite. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Second batch of the H4c reorganization. 40 engine files moved
preserving git history (git mv):
- Lights/ -> engine/include/snake3d/Lights/ (6 headers)
+ engine/src/Lights/ (4 cpp)
- Tools/ -> engine/include/snake3d/Tools/ (19 headers)
+ engine/src/Tools/ (11 cpp)
Includes inside moved files switched to <snake3d/...> angle form for
already-moved subdirs. Cross-engine references to still-root dirs
(Manager/, Physic/, etc.) kept as plain quoted root paths until those
batches move. Self-header includes in moved .cpp files updated.
Consumer rewrites in non-moved files (~35 sites): engine root headers
(Handler/Debug/*, Manager/Camera.h, Physic/Shape.h, all Renderer/*
that reference Tools::* or Lights::*), Resource/AnimLoader.h +
MaterialLoader.h, game-side (App.h, main.cpp, scenes, game meshes),
Tests/MaterialLoaderTests.cpp.
CMakeLists.txt: 40 ENGINE_SOURCES entries reprefixed to
engine/include/snake3d/ or engine/src/.
Build green, ctest 89/89.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
26 engine files moved preserving git history (git mv): - Resource/ -> engine/include/snake3d/Resource/ + engine/src/Resource/ (7 loaders/preprocessors: AnimLoader, MaterialLoader, ObjModelLoader, ResourceLoader, ShaderLoader, ShaderPreprocessor, TextureLoader) - Network/ -> engine/include/snake3d/Network/ + engine/src/Network/ (6 pairs: NetClientServer, NetClock, NetDispatcher, NetManager, NetMessages, NetProtocol) Network/Game/ remains at examples/snake3/src/Network/Game/ (game layer, unaffected). Network is fully self-contained internally; Resource has cross-subdir refs to Renderer/, Manager/ which stay as plain quoted root paths until their batches land. Consumer rewrites (~24 sites): engine root files (Manager/ResourceManager, Manager/TextureManager, Manager/ShaderRegistry, Renderer/BloomRenderer), game side (App, MainScene, MainMenuScene, TorchScene, SnakeMeshNode3D, BarrelNode3D, Network/Game/*), Tests (MaterialLoaderTests, ShaderPreprocessorTests, NetProtocolTests). CMakeLists.txt: 26 ENGINE_SOURCES entries reprefixed. Build green, ctest 89/89. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
38 engine files moved preserving git history (git mv): - Physic/ -> engine/include/snake3d/Physic/ + engine/src/Physic/ (19 files: 7 top-level shapes/checks, Algorithms/CollisionAlgorithms, Dynamics/DynamicBody, Jump/JumpTrajectory; subdirs preserved) - Handler/ -> engine/include/snake3d/Handler/ + engine/src/Handler/ (19 files: BaseHandler + BaseKeydownHandle + Debug/ stack: ImGuiOverlay, ManipulatorHandler, BaseTransform, Position/Rotation/ Scale/Collision/Lights handlers) Cross-subdir refs inside moved files: already-moved subdirs (Tools, Lights, Physic itself, Resource, Network, stdafx) -> angle includes; still-root subdirs (Manager, Renderer) -> plain quoted root paths. ImGuiOverlay (mid-normalization from inspector work) fully normalized. Consumer rewrites (~18 sites): engine root files (KeyboardManager, Scene, CollisionShape3D), game scenes (Torch/Player/RemoteSnake/ Barriers/Coin), game handlers (SnakeMove/EatLocation/Radar bases), game meshes (SnakeMeshNode3D, MarkRingNode3D), App, Tests/main.cpp. Game-layer handler files at examples/snake3/src/Handler/ stay at root (game-layer code, separate pass if needed). CMakeLists.txt: 38 ENGINE_SOURCES entries reprefixed. Build green for snake3d_engine + snake3d_game + Snake3 + Tests. ctest 89/89. Remaining: Manager (28 files), Renderer (141 files). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
28 engine files moved preserving git history (git mv): - 15 headers -> engine/include/snake3d/Manager/ (Camera, FrameUbo, KeyboardManager, MaterialPlaceholder, MaterialUbo, RenderManager, ResourceManager, ShaderFeature, ShaderProgram, ShaderRegistry, SoundManager, TextureManager, UboBindings, UniformBuffer, VboIndexer) - 13 sources -> engine/src/Manager/ Manager is a hub -- consumed by ~70 files. 101 include sites across 73 files rewritten from `"Manager/X.h"` to `<snake3d/Manager/X.h>`. Game-side `Manager/EatManager.h` and `Manager/LevelManager.h` refs (under examples/snake3/src/Manager/) intentionally left quoted -- those are game-layer files. Cross-subdir includes inside moved Manager files: Renderer (still root) kept as plain `"Renderer/Opengl/..."` quoted; Resource/Lights/ Tools/Physic/Handler/stdafx (already moved) -> angle. CMakeLists.txt: 28 ENGINE_SOURCES entries reprefixed. Build green, ctest 89/89. Remaining: Renderer (141 files, in sub-batches), then final CMakeLists target_include_directories swap + cleanup of empty root dirs. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Largest engine reorganization batch. 141 Renderer files moved
preserving git history (git mv), in 3 sub-batches:
9a (42 files): top-level renderers (Base/Bloom/DepthMap/Line/Node2D/
Node3D/PlanarReflection/RenderStats) + Scene + Model top +
Model/Utils/Collision/Debug.
9b (55 files): Material subtree (top 8 + 23 Feature + 2 Interface +
4 Particle + 12 Uniform + 4 2D).
9c (44 files): Model/Standard subtree (28 top-level meshes + 2
Animation + 14 2D).
All engine root-level subdirs (Lights/, Tools/, Resource/, Network/,
Physic/, Handler/, Manager/, Renderer/) are now EMPTY of source/
header files. Empty shell dirs remain for the Step 17 `git rm -r`
cleanup.
Cross-subdir includes inside moved Renderer files: every reference
to other already-moved engine subdir (Manager, Lights, Tools, Physic,
Resource, Handler, stdafx) is now in `<snake3d/...>` angle form.
Same-subdir siblings also use angle form where present.
Consumer rewrites: ~100 sites across game-side scenes/meshes/App/
main.cpp/Tests + a final cleanup pass that converted any lingering
root-quoted refs in 9a/9b moved files to angle form everywhere.
CMakeLists.txt: 141 ENGINE_SOURCES Renderer entries reprefixed.
Build green for snake3d_engine + snake3d_game + Snake3 + Tests.
ctest 89/89.
Remaining: Step 12 (CMakeLists target_include_directories cleanup --
drop `${CMAKE_CURRENT_SOURCE_DIR}` from snake3d_engine PUBLIC since
no engine files live at root anymore), Step 17 (`git rm -r` empty
root engine dirs).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…e dirs
Final H4c step (.claude/tasks/H4c.md Steps 12-17). With all 273 engine
files moved under engine/include/snake3d/ + engine/src/ in prior
batches, the transitional dual-include path is no longer needed.
CMakeLists.txt cleanup:
- snake3d_engine PUBLIC: drop `${CMAKE_CURRENT_SOURCE_DIR}` (repo
root). Only `engine/include` remains as the engine public surface.
- snake3d_engine PRIVATE: `${CMAKE_CURRENT_SOURCE_DIR}` kept for
Thirdparty/ single-header libs (stb_image) that live at repo root.
- snake3d_game PUBLIC: `${CMAKE_CURRENT_SOURCE_DIR}/examples/snake3/src`
added so `"Manager/EatManager.h"` / `"Manager/LevelManager.h"`
(game-layer) resolve. Inherited by Tests transitively.
- snake3d_game PRIVATE: repo root for game-side stb_image use.
Root engine directories removed (rmdir -- they were empty after
batches 1-9): Lights/, Tools/, Resource/, Network/, Physic/,
Handler/, Manager/, Renderer/ (and all their subdirs).
Final verification:
- Clean Debug build from scratch: green.
- Release smoke build: green.
- ctest: 89/89.
- find for `.h`/`.cpp` files at repo root engine dirs: zero.
- Engine public headers under engine/include/snake3d/: zero `using
namespace` (H4b regression check).
- CI workflow grep for hardcoded engine paths: zero matches.
H4c plan all 18 steps marked [x] in `.claude/tasks/H4c.md`.
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.