Commit Graph

29 Commits

Author SHA1 Message Date
Jérôme Bousquié 54a482e354 PBR 2026-09-25 14:40:47 +02:00
Jérôme Bousquié 8ece89ccba dof 2026-09-25 13:43:59 +02:00
Jérôme Bousquié 9614156848 MSAA 2026-09-25 11:20:20 +02:00
Jérôme Bousquié 35aeb769a8 refactor examples 2026-09-25 10:19:24 +02:00
Jérôme Bousquié ab3f056dbb primitive meshes 2026-09-24 14:25:44 +02:00
Jérôme Bousquié 805babe53d HDR 2026-09-24 11:21:35 +02:00
Jérôme Bousquié f15e920109 LOD GPU 2026-09-22 20:47:36 +02:00
Jérôme Bousquié 531c43a457 material batching 2026-09-22 17:07:52 +02:00
Jérôme Bousquié 3a424afe8c GPU culling 2026-09-22 15:48:15 +02:00
Jérôme Bousquié 5ae978da23 doc 2026-09-21 10:07:32 +02:00
Jérôme Bousquié ab13fa725e fix(shadow): make Étape 14 shadow visible
The shadow pass was correct but the demo light was much too steep (52°
elevation), so the blocker's shadow fell in a ~0.5-unit sliver tight against
the cube's base and was invisible against the bright ground (offscreen pixel
probe found a single dark pixel). Verified with an offscreen probe using the
real Renderer::render_scene + shadow path:
  - steep front light   (0.6,1.1,0.6)  -> 1 dark pixel   (no visible shadow)
  - shallow side light  (1.0,0.3,0.0)  -> 17 107 pixels  (shadow pipeline OK)
  - tuned front-right   (1.0,0.5,0.0)  -> 16 979 pixels  (clear visible shadow)

The azimuth matters most: from the elevated front-right camera, a shadow cast
toward -z falls behind the cube and is occluded; one cast toward -x runs across
the ground to the left of the cube and reads clearly. Tuned light therefore sits
front-right and low (toward_light (1.0,0.5,0.0)), keeping the front faces lit
while casting a clearly visible PCF-softened shadow.

Also reapply the LessEqual comparison sampler fix (commit 39167ee had set it,
but was later reverted to GreaterEqual by 9a51ff7 while debugging; the probe
confirms LessEqual is the correct, non-inverted test). Correct 'rotating cube'
to 'cube' in README/ROADMAP (shadow_test scene is static).
2026-09-19 21:12:25 +02:00
Jérôme Bousquié bfe68f4393 docs: Étape 14 (shadows) finale — bilan DRAFT, ROADMAP §4.2, README roadmap
- docs/DRAFT.md: document vidé (bilan Étape 14 archivé dans l'historique git)
- docs/ROADMAP.md §4.2: Shadows marqué [x] (Étape 14, mono-lumière PCF)
- README.md: item 12 de la roadmap (shadow mapping)
- cargo fmt --all sur les sources Étape 14
2026-09-19 19:04:12 +02:00
Jérôme Bousquié 9a51ff7602 Fix shadow_test: WebGPU clip depth, correct NdotL, shadow compare
- Use glam's directx (WebGPU) projection module for both the camera
  perspective and the shadow orthographic: NDC clip depth is [0,1] as
  wgpu expects, instead of OpenGL's [-1,1] which clipped half the frustum
  and broke depth-space consistency with the shadow map.
- Extend the shadow orthographic far plane to 2*r so the whole scene box
  (and the shadow cast behind it, toward the camera) is covered.
- Switch the shadow comparison sampler to GreaterEqual so open sky is lit
  and surfaces behind a blocker are shadowed (previous LessEqual inverted
  the shadow, blackening the entire ground and making the cube float).
- Use the surface->light direction (+position_dir) for the directional
  N*L term; the old negation darkened the cube top and lit the camera
  faces, producing the inverted-pyramid appearance.
- Drop the now-redundant [0,1] depth remap in the main-pass shader.
2026-09-19 16:10:47 +02:00
Jérôme Bousquié 67bd7af095 fix(shadow): read directional light direction from position_dir, not dir_angle
For directional lights dir_angle is Vec4::ZERO, so the old code built the
shadow light_view_proj from dir=(0,0,0), making eye==target and look_at_mat4
degenerate -> NaN light_view_proj -> compute_shadow -> fully black frame.

Directional direction lives in position_dir.xyz (surface->light); the shadow
camera looks along the light's travel direction (light->scene), i.e. the
negation, matching the DRAFT spec.
2026-09-19 13:09:39 +02:00
Jérôme Bousquié 39167ee05f fix(shadow): correct comparison sampler from GreaterEqual to LessEqual
With the shadow map cleared to 1.0 (farthest depth from light) and the
shadow pass writing smaller depths for surfaces closer to the light,
the fragment-to-light distance must be <= the stored surface depth for
lit pixels. GreaterEqual was inverted — everything appeared lit with no
shadows rendered.
2026-09-19 12:05:57 +02:00
Jérôme Bousquié c2cbd7fadb Étape 14: add shadow mapping (directional light, Phase 4.2)
Implement shadow mapping for directional lights:
- Scene::set_shadow_caster(Option<usize>) selects the shadow-casting light
  by packed frame-array index (None disables; point lights rejected at render).
- Lights::get(index) resolves a packed index across the directional/point/spot lists.
- Renderer allocates a shadow depth map, comparison sampler, group-3 bind groups,
  shadow uniform buffer and shadow pipeline; render_scene does a depth-only
  shadow pass before the main pass; compute_shadow_light_view_proj builds an
  orthographic light-space frustum from the scene radius.
- standard_shader: shadow_light_index/light_view_proj/shadow_params uniforms,
  @group(3) depth map + comparison sampler, 3x3 PCF compute_shadow().
- shadow_shader: path/vertex shader with attribute layout matching the shared
  vertex buffer (only position consumed).
- shadow_test example: directional shadow caster casts a PCF-softened shadow
  onto a ground slab; documented in examples README.
2026-09-19 09:48:17 +02:00
Jérôme Bousquié e8ff364d0d feat: lumières spot (cône + angle) (Étape 13, Phase 4.2)
- Light étendu à 4×Vec4 (64 o) : dir_angle (axe du cône + cos demi-angle)
- FrameUniforms 576→704 o : compteur num_spot, _pad[1]
- Lights + spot: Vec<Light> ; into_frame_array renvoie (arr, n_dir, n_point, n_spot)
- Scene::add_spot_light(pos, dir, color, intensity, radius, half_angle) ; clear_lights inclut spot
- standard_shader.wgsl : 3e boucle d'accumulation (pénombre lissée ±0.1 rad + atténuation linéaire)
- exemple cube : lumière spot verte pointée vers le cube
- Docs : shaders/resources README (704 B), README roadmap (item 11), ROADMAP (item coché)
- Build/test/fmt OK (5 tests + validation WGSL + doctests) ; non-régression par défaut
2026-09-18 19:05:13 +02:00
Jérôme Bousquié d518948deb feat: multi-lumières directionnelles + ponctuelles (Étape 12, Phase 4.2)
- Light (48 o) + MAX_LIGHTS=8 ; FrameUniforms étendu (ambient, lights[8], compteurs, _pad)
- resources/lights.rs : Lights (1 dir +Z par défaut, non-régression) + into_frame_array
- Scene : API déclarative add_directional_light / add_point_light / set_ambient / clear_lights
- Renderer::write_frame_uniforms upload les lumières/ambiant de la scène
- standard_shader.wgsl : struct Light + boucles d'accumulation (dir + ponctuelles, atténuation linéaire)
- exemple cube : 1 lumière ponctuelle chaude
- Docs : shaders/resources README, README roadmap, ROADMAP (item coché + Spot notée futur)
- Build/test/fmt OK ; non-régression par défaut (1 dir +Z + ambiant blanc)
2026-09-18 18:26:38 +02:00
Jérôme Bousquié b0aafefed9 feat(core): Étape 11 — resize (surface + depth, Phase 4.4) 2026-09-18 17:29:45 +02:00
Jérôme Bousquié 440f2dffa3 refactor(resources): activate diffuse textures on all render paths (Étape 10)
Implémente le plan Étape 10 (Phase 4.1 Textures), décisions D1-D4 actées :
- 10.1 : nouveau type resources::Texture (device+view+sampler), format
  Rgba8UnormSrgb, sampler linear/repeat, dep 'image' (png/jpeg). Constructeurs
  from_rgba8 / from_bytes / from_file / white_placeholder.
- 10.2 : create_texture_bind_group_layout (groupe 2 : sampler+texture, fragment).
  build_pipeline pose désormais 3 layouts [frame, object, texture] — « un seul
  layout pour tous » (D1). PipelineCache détient le layout + le placeholder blanc.
- 10.3 : shader standard — UV transmis au fragment (location 2), groupe @2
  texture_sampler + diffuse_texture, échantillonnage inconditionnel
  base = texel * couleur(vertex) (D2) : sans texture (placeholder blanc) pas
  de régression en lit comme en unlit.
- 10.4 : Material gagne texture: Option<Arc<Texture>> + texture_bind_group,
  construit dans le constructeur via le cache (layout partagé + placeholder).
- 10.5 : draw_entity bind @group(2) ; Scene : add_texture / get_texture /
  add_material_texture ; init_gpu accepte la Queue pour bâtir le placeholder.
- exemple cube : géométrie avec UV [0,1]² par face + texture damier procédurale.

Validation : fmt, check 0 warning, tests verts (3 + doc), doc sans missing_docs,
cube/simple/manual lancés sans erreur backend.
2026-09-18 14:18:02 +02:00
Jérôme Bousquié fca5d728ad refactor(renderer): activate depth buffer on all render paths (Étape 9)
- 9.1: allocate shared Depth32Float depth texture + view in Renderer,
  sized to the initial surface (create_depth_texture helper, reusable
  for the Phase 4.4 resize); Renderer::new now takes width/height.
- 9.2: attach depth_stencil_attachment (clear 1.0 / store) to both the
  low-level render and high-level render_scene passes.
- 9.3: every pipeline declares a matching DepthStencilState (write true,
  compare Less) via the shared DEPTH_FORMAT constant (D1).

Décisions D1-D4 actées 2026-09-18 (DRAFT Étape 9).
2026-09-18 13:03:52 +02:00
Jérôme Bousquié 62b5cac11b refactor(resources): Scene owns pipeline cache, Mesh->Material link
Étape 7 (DRAFT): the PipelineCache moves into the Scene (SceneGpu holds
device + format + cache, wired via Scene::init_gpu in AppRunner::resumed),
so App no longer owns a cache field. Mesh now holds Option<Arc<Material>>
(with material()/set_material()/with_material()); Entity drops material_id
({mesh_id, transform}); iter_entities yields (label, &Mesh, &Transform);
Renderer::render_scene resolves the material from the mesh or the Scene's
lazy default_material(). New declarative Scene helpers: register_shader,
add_material_shader, create_mesh. cube/simple examples migrated; manual
remains low-level and unchanged.
2026-09-17 13:15:30 +02:00
Jérôme Bousquié 0a85aff17b feat(examples): 3D MVP cube via standard shader, drop basic (Étape 5) 2026-09-17 10:40:43 +02:00
Jérôme Bousquié f10e249898 feat(renderer): active camera wired to frame uniforms (Étape 4.3)
- Camera enrichie: fov/near/far stockés, Default (pos (0,0,3), 45°, near 0.1,
  far 100), with_perspective(), projection_matrix(aspect) depuis les params
  stockés (au lieu de les passer en argument).
- Scene porte une caméra active: set_camera()/camera() (défaut Camera::default).
- Renderer::render_scene(view, scene, aspect) écrit chaque frame view/proj/
  cam_pos réels dans le buffer frame (write_frame_uniforms) avant de dessiner;
  le Renderer garde le handle du frame_buffer. Le chemin bas-niveau render()
  conserve les valeurs par défaut (identité).
- App::render_scene calcule l'aspect depuis window.inner_size() (le Renderer
  reste indépendant de la fenêtre).

Docs synchronisées: DRAFT (4.3 coche), README (statut 3D-infra + quick ref),
PLAN (caméras), ROADMAP (1.1/1.3/1.5/2.3).

Validation: check workspace+examples 0 warning, test (Pod + wgsl) OK, doc 0
warning, fmt propre. Le rendu 3D visible attend Étape 5 (brancher standard).
2026-09-16 17:25:50 +02:00
Jérôme Bousquié c1e07b42b4 feat(renderer): uniform bind groups infrastructure (Étape 3 + 4.1/4.2/4.4)
Étape 3 (infrastructure uniforms) + le câblage minimal d'Étape 4 pour garder
les exemples exécutables (wgpu requiert que tous les bind groups du layout
pipeline soient posés au draw) :

- resources/uniform.rs : types bytemuck Pod FrameUniforms (192 B) et
  ObjectUniform (64 B), alignés 16 octets sans padding; offsets vérifiés par un
  test unitaire contre le contrat du shader. glam feature bytemuck activé.
- pipeline_cache: create_uniform_bind_group_layouts() expose les 2 layouts
  (frame @0 Vertex|Fragment + object @1 Vertex); build_pipeline les attache à
  TOUT pipeline (un seul layout pour tous, décision actée).
- Renderer: alloue le buffer frame partagé + BindGroup(0) (défaut identité,
  mode lit) et un object identité partagé pour le chemin bas-niveau; cache
  RefCell<HashMap<label,(buffer,bindgroup)>> par entité, model réécrit chaque
  frame depuis transform.to_matrix(); draw_entity pose groupes 0+1.

4.3 (caméra active + aspect) non implémenté: simple/manual restent exécutables
car basic ignore ces uniforms. Documentation DRAFT mise à jour.

Validation: check workspace+examples 0 warning, doc 0 warning, test (Pod+wgsl)
OK, fmt propre.
2026-09-16 16:56:05 +02:00
Jérôme Bousquié 252db88980 feat(lib): expose Camera and Entity with Transform (data foundation)
Étape 1 du plan 3D+Phong : fondations de données sans toucher au rendu.

- resources: exporte Camera (fichier auparavant orphelin), migre les fonctions
  glam dépréciées look_at_rh/perspective_rh_gl vers glam::camera::rh::* (induit
  par la compilation de camera.rs, sinon 2 warnings).
- scene: nouveau type Entity { mesh_id, material_id, transform } (scene/entity.rs);
  Scene::entities passe de HashMap<String,(String,String)> à HashMap<String,Entity>.
- API: add_entity conserve sa signature (transform identité par défaut), ajout de
  add_entity_with_transform, entity_transform, set_entity_transform; iter_entities
  rend désormais aussi le &Transform. renderer et examples inchangés a posteriori.
- Validation: cargo check --workspace et examples 0 warning, cargo doc 0 warning, fmt OK.
2026-09-16 15:26:16 +02:00
Jérôme Bousquié 4acf1d821d feat(core): expose frame view and auto-render the scene
- AppHandler::render now receives the current &Frame; its default
  implementation renders the whole scene automatically via
  app.render_scene(frame.view()) (Option A). Users can simply not
  implement render for full auto-rendering.
- Add Renderer::render_scene: batch-renders every scene entity in a
  single render pass. Factored per-mesh draw logic into a private
  draw_entity helper shared with Renderer::render.
- Add App::render_scene(view) delegating to the Renderer.
- Fill simple.rs with a real quad (mesh/material/entity) without
  importing wgpu; the scene now auto-renders via the trait default.
2026-09-16 10:36:35 +02:00
Jérôme Bousquié 7b25564483 doc 2026-07-08 15:46:50 +02:00
Jérôme Bousquié 82ea16d118 re-org en App 2026-07-07 16:42:37 +02:00