Skip to content

Feature/physics#47

Open
ViPErCZ wants to merge 89 commits into
masterfrom
feature/physics
Open

Feature/physics#47
ViPErCZ wants to merge 89 commits into
masterfrom
feature/physics

Conversation

@ViPErCZ
Copy link
Copy Markdown
Owner

@ViPErCZ ViPErCZ commented May 20, 2026

No description provided.

ViPErCZ and others added 30 commits May 13, 2026 10:53
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>
ViPErCZ and others added 30 commits May 27, 2026 19:01
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant