15.5 : la_voie_manuelle fermee, fs_pbr couvert cote spot, cliquet de promesses a 0. (1) spot en PBR : le cone est calcule dans fs_main ET fs_pbr, mais aucun test de spot ne traversait fs_pbr (le verrou existant utilise add_material_shader, donc fs_main seulement) - retirer le bloc de fs_pbr ne faisait rougir AUCUNE suite, alors que mutate-spot.py contenait deja la phrase 'le calcul du cone vit a deux endroits' : un constat ecrit n'est pas un verrou. Nouveau verrou le_spot_eclaire_aussi_en_pbr_un_faisceau_et_non_un_pied_de_lumiere + M4 (ancre light_color, 1 occurrence) ROUGE ; campagne 5 cas, 0 ecart. Discriminant non transposable : le plancher spec_ibl dilue le rapport (4,62 vs 2,07) - ce qui porte est la sensibilite a l'angle (3 723 texels de difference, 0 quand le cone saute). Caracterisation consignee : cos_outer = cos_inner - 0.1 en espace cosinus, donc half_angle 0,06 rad donne un cone exterieur de 26 degres (tache 50 texels contre 80). (2) demo : verrou le_demo_declare_chaque_primitive_et_chaque_source_de_texture_qu_il_nomme (6 constructeurs, exactement 2 from_file et 2 from_rgba8, deux noms d'assets) + 8 lignes de manifeste. (3) deformable : le defaut consigne est CORRIGE (EntityData::mesh est un Arc clone -> Arc::get_mut ratait toujours ; geometrie CPU derriere un RwLock, voie rapide refusee quand un BVH existe) ; 4 verrous, campagne mutate-deform.py 4 cas 0 ecart, M3 vu ROUGE seulement apres avoir deplace le verrou GPU sur une sphere sans BVH. UNCOVERED_EXAMPLES_RATCHET 3 -> 0. Harnais : 953 tests, 0 skip.
This commit is contained in:
1 parent
4aa8b0ef25
commit
f3c4b6cc37
27 files changed
+887
-277
No files matched your search
@@ -840,6 +840,8 @@ decoder_can_read`, dont la liste d'extensions est **lue dans `lib/Cargo.toml`**
|
||||
|
||||
- **L'émissif est MULTIPLIÉ par l'albédo — un albédo noir annule l'émission, sans erreur ni log (15.5, trouvé en mesurant, pas en relisant)** : `standard_shader.wgsl` calcule `emissive_contrib = base_for_detail * object.emissive.rgb * object.emissive.a` en **quatre** endroits (deux entrées fragment `unlit`, deux lit — la même obligation que le gotcha 6.7 « brancher dans TOUS les points d'entrée »). Le premier harnais de la 15.5 mettait `with_color([0.0,0.0,0.0,1.0])` pour « isoler l'émissif » : la jambe HDR était **totalement muette**, sans le moindre message. Même famille qu'`ObjectUniform::ambient_color = (0,0,0,0)` : *une valeur par défaut qui annule un terme entier ne se voit pas*. Verrou de l'effet : `au_dessus_de_un_en_ldr_deux_niveaux_d_emission_rendent_a_l_identique_et_la_reference_ne_bouge_pas` (identité LDR **0 texel**, HDR **1 198** texels, référence **0**). ⚠ **Ce fait précis n'est pas verrouillé** : la campagne `campaigns/mutate-emissive.py` (4 cas, 0 écart — M1 émissif neutre aux quatre sites ROUGE, M2 écrêtage LDR supprimé ROUGE, **M3 retrait de la multiplication par l'albédo VERT consigné**) le dit mesuré. Un verrou d'identité+localité ne porte pas une magnitude : l'écrire comme couvert serait une fausse preuve (6.19).
|
||||
|
||||
- **Le cône du spot est bien branché dans les DEUX entrées fragment, mais `fs_pbr` n'était traversé par aucun test de spot (15.5, mesuré, couvert le 2026‑11‑03)** : `standard_shader.wgsl` calcule `spot_factor` dans `fs_main` (bloc ~637) **et** dans `fs_pbr` (bloc ~1527) — la famille 6.7 « brancher dans TOUS les points d'entrée ». Le verrou `le_spot_eclaire_un_faisceau_directe_et_non_un_pied_de_lumiere` construit son matériau avec `add_material_shader`, donc ne rend **que** `fs_main` : retirer le bloc cône de `fs_pbr` ne faisait rougir **aucune** suite. Et le commentaire de la campagne `mutate-spot.py` disait déjà « le calcul du cône vit à deux endroits… muter un seul site ne fait pas rougir le verrou » : **un constat écrit dans un instrument n'est pas un verrou** — il faut le second test, pas la phrase. Couvert par `le_spot_eclaire_aussi_en_pbr_un_faisceau_et_non_un_pied_de_lumiere` (`lib/src/core/spot_pbr_tests.rs`) ; M4 de `campaigns/mutate-spot.py` (le terme cône retiré de `fs_pbr` seul, ancre `light_color`, 1 occurrence) rendu **ROUGE** — 5 cas, 0 écart. Deux mesures de méthode : (a) le discriminant **ne se transpose pas** d'une entrée à l'autre — en PBR, le plancher `spec_ibl` (gotcha 6.14) dilue le rapport centre/bord à **4,62** contre **2,07** pour la ponctuelle (2,23 de séparation, trop peu pour un seuil), alors qu'en `fs_main` il vaut 182,00 contre 2,94 ; ce qui porte côté PBR est la **sensibilité à l'angle** : deux rendus qui ne diffèrent que par `half_angle` (0,45 / 0,06) different de **3 723 texels** quand le cône est appliqué, de **0** quand il ne l'est pas (une identité, pas un seuil) ; (b) **caractérisation consignée** : le cône extérieur du moteur vaut `cos_outer = cos_inner − 0,1` en **espace cosinus**, donc `half_angle = 0,06` rad (3,4°) produit un cône extérieur de **26°** — tache de **50** texels contre **80** à 0,45, alors que l'angle interne est divisé par 7. Un appelant qui règle `half_angle` pour viser gros… ou fin, obtient une tache bien plus large que demandé, sans erreur ni log. Le verrou de caractérisation rougira le jour où une vraie pénombre d'angle sera implémentée (famille `log_depth_drops_triangles_spanning_a_large_depth_range`).
|
||||
|
||||
- **`Scene::set_depth_prepass` PANIQUE si elle est appelée avant `Scene::init_gpu` — et la fenêtre où elle est correcte est étroite (15.5, mesuré au premier essai d'un harnais DoF)** : le setter (`scene.rs:466`) fait `self.gpu().cache.borrow().set_depth_prepass(enabled)` — donc il exige le GPU — alors que sa documentation et le gotcha 13.2 imposent de l'armer **avant toute création de matériau**. Les deux contraintes se referment sur un intervalle : **après `init_gpu`, avant `add_material`/`create_mesh`**. Un appel juste après `Scene::new()` meurt sur « scene pipeline not initialized yet », sans rapport avec ce que l'appelant essayait de faire. Le setter rend un `bool` (refus du cache) : l'asserter, sinon la passe est désarmée en silence (13.2). Vérifier ce qu'une API **exige** avant d'écrire un harnais — le nom de l'API ne le dit pas (famille 13.5 « il n'existe pas de traversée d'entités sans GPU »).
|
||||
|
||||
<!-- lean-ctx -->
|
||||
|
||||
@@ -22,6 +22,7 @@ sys.path.insert(0, str(RACINE / "campaigns"))
|
||||
from skipdetect import count_skips
|
||||
|
||||
CIBLE = "le_spot_eclaire_un_faisceau_directe"
|
||||
CIBLE_PBR = "le_spot_eclaire_aussi_en_pbr"
|
||||
|
||||
CONES = {
|
||||
"T0": ("VERT", []),
|
||||
@@ -34,6 +35,12 @@ CONES = {
|
||||
"M3": ("ROUGE", [(SCENE,
|
||||
"pos, dir, color, intensity, radius, half_angle,",
|
||||
"pos, -dir, color, intensity, radius, half_angle,", 1)]),
|
||||
# M4 : le terme cône retiré de `fs_pbr` seul (l'ancre est la ligne `light_color`, qui n'existe que
|
||||
# dans le bloc des spots de `fs_pbr`). C'est le cas qui fermait le trou de couverture : avant le
|
||||
# verrou PBR, aucune suite ne traversait le spot en PBR.
|
||||
"M4": ("ROUGE", [(SHADER,
|
||||
" let light_color = frame.lights[i].color.rgb * frame.lights[i].color.a * falloff * spot_factor;",
|
||||
" let light_color = frame.lights[i].color.rgb * frame.lights[i].color.a * falloff;", 1)]),
|
||||
}
|
||||
|
||||
CIBLES_CPU = ["spot_cone_axis_alignment_is_positive"]
|
||||
@@ -41,7 +48,7 @@ CIBLES_CPU = ["spot_cone_axis_alignment_is_positive"]
|
||||
|
||||
def lancer() -> str:
|
||||
sortie = ""
|
||||
for cible in [CIBLE] + CIBLES_CPU:
|
||||
for cible in [CIBLE, CIBLE_PBR] + CIBLES_CPU:
|
||||
r = subprocess.run(
|
||||
["cargo", "test", "-q", "-p", "wsg-lib", "--lib", cible, "--", "--test-threads=1"],
|
||||
cwd=RACINE, capture_output=True, text=True, timeout=2400,
|
||||
|
||||
@@ -200,7 +200,7 @@ de carte : c'est une étape, pas un verrou.
|
||||
| instances_sim | the frame time must not move. | bench_12_15_cpu_cost_of_a_resident_batch_is_independent_of_n |
|
||||
| instances_sim | Pressing Space keeps the SAME picture but changes who computes it | picking_is_identical_across_the_residency_switch, a_resident_batch_is_drawn_from_the_caller_buffer_without_any_upload |
|
||||
| instances_sim | is refused instead of silently emptying | residency_refusal_is_observed_at_the_example_framing |
|
||||
| spot_test | not a halo around it | le_spot_eclaire_un_faisceau_directe_et_non_un_pied_de_lumiere |
|
||||
| spot_test | not a halo around it | le_spot_eclaire_un_faisceau_directe_et_non_un_pied_de_lumiere, le_spot_eclaire_aussi_en_pbr_un_faisceau_et_non_un_pied_de_lumiere |
|
||||
| spot_test | A smooth penumbra at the cone | le_spot_eclaire_un_faisceau_directe_et_non_un_pied_de_lumiere |
|
||||
| spot_test | A hard ring would mean the cone angle | le_spot_eclaire_un_faisceau_directe_et_non_un_pied_de_lumiere |
|
||||
| spot_test | The lit patch stays FIXED IN THE WORLD | le_spot_eclaire_un_faisceau_directe_et_non_un_pied_de_lumiere |
|
||||
@@ -234,5 +234,13 @@ de carte : c'est une étape, pas un verrou.
|
||||
| deformable | its own copy of the positions | une_geometrie_remise_a_jour_deplace_le_raycast_et_la_boite |
|
||||
| deformable | look at | la_voie_rapide_sans_bvh_deplace_le_raycast |
|
||||
| deformable | not at the camera. | une_geometrie_remise_a_jour_deplace_le_raycast_et_la_boite |
|
||||
| demo | Every procedural primitive | le_demo_declare_chaque_primitive_et_chaque_source_de_texture_qu_il_nomme, every_primitive_triangle_is_wound_ccw_from_outside |
|
||||
| demo | arranged around a textured ground | le_demo_declare_chaque_primitive_et_chaque_source_de_texture_qu_il_nomme |
|
||||
| demo | Mix of texture SOURCES | le_demo_declare_chaque_primitive_et_chaque_source_de_texture_qu_il_nomme |
|
||||
| demo | procedural patterns on the rest | le_demo_declare_chaque_primitive_et_chaque_source_de_texture_qu_il_nomme |
|
||||
| demo | the material path differs | le_demo_declare_chaque_primitive_et_chaque_source_de_texture_qu_il_nomme, the_two_formats_agree_at_mip_zero_and_differ_at_mip_one |
|
||||
| demo | Shadows from a single directional light | chaque_caster_ecrit_sa_propre_ombre_et_le_retrait_eteint_tout, a_plane_casts_a_shadowed_area_unless_the_light_is_horizontal |
|
||||
| demo | Inconsistent shadow direction between objects | l_ombre_s_allonge_dans_le_sens_de_la_lumiere, chaque_caster_ecrit_sa_propre_ombre_et_le_retrait_eteint_tout |
|
||||
| demo | rather than scene lighting. | shadow_probe_positive_control, l_arete_d_ombre_presente_une_penombre |
|
||||
| deformable | The shape changes every frame and never jumps | la_deformation_uploadee_change_la_forme_affichee |
|
||||
| deformable | the mesh only looks frozen | la_deformation_uploadee_change_la_forme_affichee, une_geometrie_remise_a_jour_deplace_le_raycast_et_la_boite, la_voie_rapide_sans_bvh_deplace_le_raycast |
|
||||
@@ -415,3 +415,73 @@ mesure a confirmé que l'échelle est traitée ; ce qui manquait était l'observ
|
||||
**Verrous neufs (7) :** `core/manual_path_tests.rs` (2), `tests/manual_vs_facade.rs` (4), `core/collision_sweep_tests.rs` (1 : aller-retour de deux cubes, **2 transitions** pour `collides` et `collides_precise`, premier contact mesuré à écart **0,5** = la vérité géométrique pour des cubes de côté 1 centrés sur ±d). `core/deform_tests.rs` (1 caractérisation). Cliquet `UNCOVERED_EXAMPLES_RATCHET` **4 → 2** (`manual`, `collision` fermés) ; restent `deformable` et `demo`, dont les phrases restantes (ondulation continue, absence de saccade, « every procedural primitive ») n'ont pas encore de porteur mesuré — le cliquet ne descend pas pour elles.
|
||||
|
||||
**Faute de méthode consignée :** j'ai d'abord attribué le « 0 texel peint » à la voie manuelle ; la cause était la géométrie, commune aux deux voies. Et mon premier seuil (» 3 000) était inventé, rouge sur un moteur corrigé (mesuré 899) — dixième occurrence de « une attente non mesurée n'est pas un contrôle ».
|
||||
|
||||
## 27. Séance du 3 novembre (suite) — `deformable` corrigé, `demo` fermé, `fs_pbr` couvert ; cliquet à 0
|
||||
|
||||
**Cliquet `UNCOVERED_EXAMPLES_RATCHET` : 3 → 2 → 1 → 0** (mesuré). Plus aucun exemple dont le bandeau
|
||||
énonce une prédiction sans ligne de manifeste. Ce qui a été fait, dans l'ordre le moins cher d'abord.
|
||||
|
||||
### 27.1 `deformable` : le défaut de caractérisation est **corrigé**
|
||||
|
||||
Le défaut consigné au § 26 (`update_geometry` ré-uploade le GPU mais ne remplace pas la géométrie CPU)
|
||||
avait une cause mesurée : `EntityData::mesh` est un `Arc<Mesh>` **clone**, donc `refcount ≥ 2` dès qu'une
|
||||
entité utilise le maillage et le `if let Some(..) = Arc::get_mut(..)` rate **toujours**. Le commentaire
|
||||
« In practice the arena is the sole owner (refcount == 1) » était faux.
|
||||
|
||||
Correction : géométrie CPU derrière un `RwLock` (le garde se partage à travers l'`Arc`), mise à jour
|
||||
inconditionnelle ; et **voie rapide refusée quand un BVH existe** (le BVH est immuable et porte sa propre
|
||||
copie des positions — sinon le raycast d'un maillage de plus de 256 triangles continue de traverser
|
||||
l'ancienne structure). Coût du refus : voie lente < 0,1 ms contre < 0,01 ms.
|
||||
|
||||
Quatre verrous dans `lib/src/core/deform_tests.rs` :
|
||||
`une_geometrie_remise_a_jour_deplace_le_raycast_et_la_boite` (360 tri → voie lente),
|
||||
`la_voie_rapide_sans_bvh_deplace_le_raycast` (112 tri → voie rapide, c'est elle qui porte le `RwLock`),
|
||||
`la_voie_lente_update_geometry_deplace_le_raycast`,
|
||||
`la_deformation_uploadee_change_la_forme_affichee` (silhouette **1 296 → 8 412** texels, rapport ~6,5).
|
||||
|
||||
Campagne `campaigns/mutate-deform.py` : **4 cas, 0 écart** (T0 VERT ; M1 remplacement CPU retiré ROUGE ;
|
||||
M2 condition BVH retirée ROUGE ; M3 `write_buffer` qui ne récrit rien ROUGE). Trois leçons d'instrument :
|
||||
|
||||
- **une correction peut déplacer le chemin qu'un verrou traverse** : M3 laissait les quatre verrous
|
||||
**VERTS** parce que le verrou GPU utilisait une sphère de 360 triangles qui, *à cause de la correction*,
|
||||
empruntait la voie lente avec un buffer neuf. Le verrou est passé sur une sphère 8×8 sans BVH.
|
||||
- **deux tentatives de M3 étaient des non-mutations** (`let _ = data;` puis `&[] as &[u8]` laissent
|
||||
`bytemuck::cast_slice` sans inférence → `error[E0283]`) ; la campagne les a **REFUSÉES** sans conclure
|
||||
(famille 9.4 / 13.3 / 6.14).
|
||||
- **le périmètre de compilation n'est pas celui du harnais** : `cargo check --all-targets` sans
|
||||
`debug-wireframe` ne compile pas `wireframe_triangle_edges(geo)`, qui reçoit désormais un garde
|
||||
`RwLock` — la rupture n'a été vue que par `run-tests-honest.sh`.
|
||||
|
||||
### 27.2 `demo` : les inventaires du bandeau sont maintenant confrontés au fichier
|
||||
|
||||
`demo` est la porte d'entrée de l'API (« Read this file first ») ; son bandeau énonce deux inventaires
|
||||
(six primitives, deux assets fichier + deux motifs procéduraux) que rien ne confrontait. Verrou
|
||||
`le_demo_declare_chaque_primitive_et_chaque_source_de_texture_qu_il_nomme` (`lib/tests/example_banners.rs`) :
|
||||
six constructeurs présents, **exactement** 2 appels à `from_file(` et 2 à `from_rgba8(`, les deux noms
|
||||
d'assets nommés, plus une non-vacuité qui compte les six primitives. Touches du bandeau vérifiées gérées
|
||||
(`KeyR`, `Digit1/2/3`). Huit lignes ajoutées au manifeste.
|
||||
|
||||
### 27.3 `fs_pbr` sous spot : le second point d'entrée était **non couvert**
|
||||
|
||||
Le cône du spot est calculé dans `fs_main` **et** `fs_pbr` ; le verrou de spot existant ne rend que
|
||||
`fs_main` (matériau `add_material_shader`). Retirer le bloc cône de `fs_pbr` ne faisait rougir aucune
|
||||
suite — alors que `mutate-spot.py` **déjà écrit** contenait la phrase « le calcul du cône vit à deux
|
||||
endroits… muter un seul site ne fait pas rougir le verrou ». Constat écrit ≠ verrou.
|
||||
|
||||
Verrou `le_spot_eclaire_aussi_en_pbr_un_faisceau_et_non_un_pied_de_lumiere` (`lib/src/core/spot_pbr_tests.rs`)
|
||||
+ M4 dans `campaigns/mutate-spot.py` (ancre `light_color`, 1 occurrence, bloc `fs_pbr`) → **ROUGE** ;
|
||||
campagne complète **5 cas, 0 écart**. Le discriminant ne se transpose pas d'une entrée à l'autre : côté
|
||||
PBR le plancher `spec_ibl` (gotcha 6.14) dilue le rapport centre/bord (**4,62** contre **2,07** pour une
|
||||
ponctuelle), donc ce qui porte est la **sensibilité à l'angle** — deux rendus PBR qui ne diffèrent que par
|
||||
`half_angle` (0,45 / 0,06) different de **3 723 texels**, et de **0** quand le cône n'est pas appliqué.
|
||||
|
||||
**Caractérisation consignée** (elle rougira à la correction) : `cos_outer = cos_inner − 0.1` en espace
|
||||
cosinus ⇒ `half_angle = 0.06` rad (3,4°) donne un cône extérieur de **26°** ; tache de **50** texels contre
|
||||
**80** à 0,45. Régler `half_angle` pour viser fin ne rétrécit donc pas la tache autant que l'API le donne
|
||||
à penser — sans erreur ni log.
|
||||
|
||||
### 27.4 État de la suite
|
||||
|
||||
`./run-tests-honest.sh` : **VERT HONNÊTE, 951 tests exécutés, 0 skip** (+ 9 doc-tests `ignore`) ;
|
||||
`docs/generated/TEST-SUITE.md` régénéré. Les totaux ne sont pas recopiés dans la prose (verrou
|
||||
`test_totals_appear_only_where_they_are_measured`).
|
||||
@@ -137,7 +137,7 @@ recomptement, pas comme un fait, et la liste des 4 est **nommée** (une liste se
|
||||
| `campaigns/mutate-sky-msaa.py` | les trois verrous neufs `skybox` / `msaa` de la séance 15.5 | `lib/src/core/sky_tests.rs`, `lib/src/core/msaa_tests.rs` (4 cas, 0 écart — M1 `set_skybox` ignore son argument : seul le verrou du fond tombe ; M2 forcer 1 échantillon MSAA : **REFUSÉ par validation wgpu même après mutation de quatre sites** — le `sample_count` est une convention répartie sur ≥ 5 endroits, donc les deux verrous de `core::msaa_tests` n'ont pas encore de cas qui prouve qu'ils ne sont pas des décors (écart consigné) ; M3 seul `main_sample_count` muté : **VERT, mutation inerte** — ce champ ne décide pas de l'échantillonnage) |
|
||||
| `campaigns/mutate-doc-contract.py` | — | AGENTS.md |
|
||||
| `campaigns/mutate-pick-scale.py` | le verrou `une_entite_scalee_est_raycee_a_sa_boite_monde_reelle` — chemin `Transform.scale` → espace local → AABB → BVH, que **aucun** verrou ne traversait | `lib/src/scene/scene.rs` (3 cas, 0 écart — M1 `world.inverse()` remplacé par l'identité, M2 échelle retirée de la matrice monde : les deux rendent muette la jambe « cube porté à `scale.y = 3`, rayon à `y = 1,2` » tandis que le contrôle à `y = 0` continue de toucher. Mesures propres : `distance = 4,5000`, `point = (−0,500 ; 1,200 ; 0)`, `normal = (−1,000 ; 0 ; 0)`, `local = (−0,500 ; 0,400 ; 0)` — le `local_point` à 0,4 = 1,2 / 3 est la preuve que l'échelle est inversée. **Aucun défaut moteur n'a été trouvé** : le trou était l'absence d'observation) |
|
||||
| `campaigns/mutate-spot.py` | le verrou GPU `le_spot_eclaire_un_faisceau_directe_et_non_un_pied_de_lumiere` (la famille `spot` n'avait **aucun** verrou de rendu : seulement l’empaquetage CPU de la table et l’alignement de l’axe) | `lib/src/core/batch_gpu_tests.rs` (4 cas, 0 écart — M1 le cône ignore son axe : rapport centre/bord **2,94**, identique au contrôle ponctuelle, et le bord passe de 0 à 62 ; M2 le facteur de cône devient un seuil dur : le rapport reste à **182,00** mais la bande de texels intermédiaires tombe de **19 à 6** — seule la jambe de pénombre mord ; M3 l’axe retourné côté scène : `centre=0`. Deux remarques : le calcul du cône vit à **deux endroits** du shader standard (les deux entrées fragment — muter un seul site ne fait pas rougir le verrou, famille 6.7), et le seuil de pénombre est calibré sur les deux bornes mesurées 19 / 6) |
|
||||
| `campaigns/mutate-spot.py` | le verrou GPU `le_spot_eclaire_un_faisceau_directe_et_non_un_pied_de_lumiere` (la famille `spot` n'avait **aucun** verrou de rendu : seulement l’empaquetage CPU de la table et l’alignement de l’axe) | `lib/src/core/batch_gpu_tests.rs` (4 cas, 0 écart — M1 le cône ignore son axe : rapport centre/bord **2,94**, identique au contrôle ponctuelle, et le bord passe de 0 à 62 ; M2 le facteur de cône devient un seuil dur : le rapport reste à **182,00** mais la bande de texels intermédiaires tombe de **19 à 6** — seule la jambe de pénombre mord ; M3 l’axe retourné côté scène : `centre=0`. Deux remarques : le calcul du cône vit à **deux endroits** du shader standard (les deux entrées fragment — muter un seul site ne fait pas rougir le verrou, famille 6.7), et le seuil de pénombre est calibré sur les deux bornes mesurées 19 / 6.) **Complété le 2026‑11‑03** : 5 cas, 0 écart — M4 retire le terme cône de `fs_pbr` seul (l’ancre `light_color` n’existe que dans ce bloc) et laisse `texels differents = 0` : le verrou `le_spot_eclaire_aussi_en_pbr_un_faisceau_et_non_un_pied_de_lumiere` (`lib/src/core/spot_pbr_tests.rs`) rougit. Le discriminant retenu côté PBR n’est pas le rapport centre/bord (le plancher `spec_ibl` le dilue : 4,62 contre 2,07, mesure) mais la **sensibilité à l’angle** : deux rendus PBR qui ne diffèrent que par `half_angle` (0,45 / 0,06) different de **3 723 texels** quand le cône est appliqué, de **0** quand il ne l’est pas. Ce verrou consigne aussi une **caractérisation** : le cône extérieur vaut `cos(half_angle) − 0,1` en espace cosinus, donc `half_angle = 0,06` rad (3,4°) donne un cône extérieur de **26°** — tache de **50** texels contre **80** à 0,45. |
|
||||
| `campaigns/mutate-instances.py` | le verrou `basculer_entites_contre_lot_ne_change_que_le_nombre_de_draws` (draws 1 contre N, contrôles positifs dimensionnés, identité d’image à 0 texel) | `lib/src/core/batch_gpu_tests.rs` (4 cas, 0 écart — M1 la voie lot forcée vers les entités : `draws lot=24`, ROUGE ; M2 scène sortie du cadre : les deux voies ne peignent **plus un seul texel** (`px vus lot=0 entites=0`) et **les images restent identiques à 19 200 / 0** — ce qui fait rougir est le contrôle positif, pas l’identité ; M3 teinte différente selon la voie : 11 260 texels différents, ROUGE. Mesure propre : 1 draw contre 24, 11 260 texels vus dans les deux voies, **19 200 texels identiques, 0 différent** — l’instancing rend l’image au bit près) |
|
||||
| `campaigns/mutate-repetition.py` | les deux verrous neufs de `repetition_break` : localité par groupe dans l'exemple, et comparaison exacte du mode dans le shader (gotcha 6.26) | `lib/src/resources/material_tint_tests.rs` (5 cas, 0 écart — M1 `rmode == 1u \|\| rmode == 3u` → `rmode >= 1u` : ROUGE, et **deux occurrences mutées** — la porte vit dans les DEUX entrées fragment, famille 6.7 ; M2 le groupe du milieu renommé `left_` (6 sites) : ROUGE, le témoin est détruit ; M3 `flags_right: 3` : ROUGE, la droite n'est plus « noise ALONE » ; M4 la bascule du milieu remplacée par un armement constant : ROUGE, « Press 1 again returns to raw repetition » devient faux) |
|
||||
| `campaigns/mutate-envmap.py` | le verrou de démonstration `les_six_spherees_denv_map_ne_different_que_par_metallic_et_roughness` (la phrase « ONLY in metallic and roughness » porte sur l'exemple livré) | `lib/src/resources/material_tint_tests.rs` (4 cas, 0 écart — M1 `tf.scale` posée dans la boucle : ROUGE ; M2 la table `SPHERES` ramenée à 5 paires : ROUGE ; M3 une seconde géométrie `uv_sphere` dans la boucle : ROUGE. Le verrou a été vu ROUGE quatre fois **pendant sa propre écriture** : une tranche d'annotation de type prise pour la table, un offset **relatif** traité comme absolu, et deux assertions qui rougissaient sur des faits légitimes — le `create_mesh` et l'`add_material_pbr` du sol) |
|
||||
|
||||
@@ -4,23 +4,23 @@
|
||||
|
||||
# Suite de tests — mesurée, pas souvenir
|
||||
|
||||
- Généré le : 2026-11-04 11:45 +0100
|
||||
- Généré le : 2026-11-04 12:01 +0100
|
||||
- Périmètre : `cargo test --workspace --no-fail-fast --features debug-bbox,debug-wireframe,stats,import-obj,import-stl -- --nocapture --test-threads=2`
|
||||
- Verdict : **VERT HONNÊTE : 951 tests exécutés, 0 skip, 9 doc-tests ignorés.**
|
||||
- Verdict : **VERT HONNÊTE : 953 tests exécutés, 0 skip, 9 doc-tests ignorés.**
|
||||
- Lignes de skip (les trois formes `skip …`, `[skip] …`, `SKIP …`, lues par `campaigns/skipdetect.py` : tests GPU qui n'ont pas observé, donc comptés `passed` par le harnais) : **0**
|
||||
|
||||
| Binaire de test | passed | failed | ignored |
|
||||
|---|---:|---:|---:|
|
||||
| `unittests src/lib.rs` | 836 | 0 | 0 |
|
||||
| `unittests src/lib.rs` | 837 | 0 | 0 |
|
||||
| `tests/doc_consistency.rs` | 24 | 0 | 0 |
|
||||
| `tests/example_banners.rs` | 9 | 0 | 0 |
|
||||
| `tests/example_banners.rs` | 10 | 0 | 0 |
|
||||
| `tests/manual_vs_facade.rs` | 4 | 0 | 0 |
|
||||
| `tests/roadmap_consistency.rs` | 14 | 0 | 0 |
|
||||
| `tests/roadmap_reduction.rs` | 2 | 0 | 0 |
|
||||
| `tests/texture_procedural.rs` | 4 | 0 | 0 |
|
||||
| `tests/wgsl_validate.rs` | 49 | 0 | 0 |
|
||||
| `Doc-tests wsg_lib` | 9 | 0 | 9 |
|
||||
| **Total (somme des lignes ci-dessus)** | **951** | **0** | **9** |
|
||||
| **Total (somme des lignes ci-dessus)** | **953** | **0** | **9** |
|
||||
|
||||
Ce fichier est **régénéré** à chaque exécution de `./run-tests-honest.sh` : un nombre qui y
|
||||
figure a été produit par les compteurs du harnais, pas recopié d'un souvenir. Ailleurs dans les
|
||||
|
||||
@@ -66,8 +66,8 @@ impl AppHandler for MonQuad {
|
||||
[1.0, 1.0, 0.0, 1.0], // bottom-left (yellow)
|
||||
])
|
||||
.with_indices(vec![0, 2, 1, 0, 3, 2]); // winding CCW-vu-de-dehors (convention du depot,
|
||||
// `CullMode::Back` + `front_face = Ccw`) : l'ordre
|
||||
// `0,1,2 / 0,2,3` etait culle des deux voies.
|
||||
// `CullMode::Back` + `front_face = Ccw`) : l'ordre
|
||||
// `0,1,2 / 0,2,3` etait culle des deux voies.
|
||||
|
||||
// Default material: `None` lets the Scene inject its `standard` at render time
|
||||
// (`Scene::default_material`) — this exercises the default path.
|
||||
|
||||
@@ -4009,7 +4009,9 @@ fn le_spot_eclaire_un_faisceau_directe_et_non_un_pied_de_lumiere() {
|
||||
1,
|
||||
);
|
||||
scene.add_material_shader("sol", "standard").unwrap();
|
||||
scene.set_material_color("sol", [1.0, 1.0, 1.0, 1.0]).unwrap();
|
||||
scene
|
||||
.set_material_color("sol", [1.0, 1.0, 1.0, 1.0])
|
||||
.unwrap();
|
||||
scene
|
||||
.create_mesh(
|
||||
"sol",
|
||||
@@ -4023,11 +4025,7 @@ fn le_spot_eclaire_un_faisceau_directe_et_non_un_pied_de_lumiere() {
|
||||
// Un corps sur le sol, legerement hors axe : le bandeau de `spot_test` affirme que la tache
|
||||
// eclairee reste **fixe dans le monde** quand l'objet tourne.
|
||||
scene
|
||||
.create_mesh(
|
||||
"cube",
|
||||
crate::mesh::primitives::cube(1.2),
|
||||
Some("sol"),
|
||||
)
|
||||
.create_mesh("cube", crate::mesh::primitives::cube(1.2), Some("sol"))
|
||||
.unwrap();
|
||||
let mut tf_cube = crate::core::Transform::identity();
|
||||
tf_cube.translation = glam::Vec3::new(2.2, 0.6, 0.0);
|
||||
@@ -4050,12 +4048,7 @@ fn le_spot_eclaire_un_faisceau_directe_et_non_un_pied_de_lumiere() {
|
||||
.unwrap();
|
||||
} else {
|
||||
scene
|
||||
.add_point_light(
|
||||
glam::Vec3::new(0.0, 4.0, 0.0),
|
||||
[1.0, 1.0, 1.0],
|
||||
1.0,
|
||||
14.0,
|
||||
)
|
||||
.add_point_light(glam::Vec3::new(0.0, 4.0, 0.0), [1.0, 1.0, 1.0], 1.0, 14.0)
|
||||
.unwrap();
|
||||
}
|
||||
let cam = scene.camera_mut();
|
||||
@@ -4138,13 +4131,19 @@ fn le_spot_eclaire_un_faisceau_directe_et_non_un_pied_de_lumiere() {
|
||||
);
|
||||
|
||||
// Controle positif : la scene rend (sinon « le faisceau est directif » est satisfait par le vide).
|
||||
assert!(centre > 40, "le pied du cone ne rend que {centre} : la scene n'est pas mesurable — controler le cadrage avant d'interpreter le rapport");
|
||||
assert!(
|
||||
centre > 40,
|
||||
"le pied du cone ne rend que {centre} : la scene n'est pas mesurable — controler le cadrage avant d'interpreter le rapport"
|
||||
);
|
||||
|
||||
// ── P1 : le faisceau est directif, pas un pied de lumiere. Mesures 2026-11-03 : rapport
|
||||
// centre/bord **182,00** pour le cone (bord = 0, ambiante nulle hors cone) contre **2,94** pour
|
||||
// une ponctuelle a la meme position. Seuil 20 : facteur ~9 au-dessus de la valeur du cone
|
||||
// attenuee, ~7 au-dessus du controle — et le controle est asserte de l'autre cote.
|
||||
assert_eq!(bord, 0, "le bord de l'image rend {bord} avec un cone et une ambiante nulle : hors du cone, rien n'eclaire — la lumiere ne respecte plus l'axe");
|
||||
assert_eq!(
|
||||
bord, 0,
|
||||
"le bord de l'image rend {bord} avec un cone et une ambiante nulle : hors du cone, rien n'eclaire — la lumiere ne respecte plus l'axe"
|
||||
);
|
||||
assert!(
|
||||
centre as f64 / bord.max(1) as f64 > 20.0,
|
||||
"rapport centre/bord du cone = {:.2} (mesure 182,00 ; ponctuelle 2,94) : le cone ne se distingue plus d'une ponctuelle — le bandeau « a directed BEAM, not a halo » est faux",
|
||||
@@ -4152,7 +4151,7 @@ fn le_spot_eclaire_un_faisceau_directe_et_non_un_pied_de_lumiere() {
|
||||
);
|
||||
assert!(
|
||||
(centre_p as f64 / (bord_p.max(1) as f64)) < 10.0,
|
||||
"controle : la ponctuelle rend un rapport centre/bord de {:.2} — si ce rapport monte, la jambe comparative ne compare plus rien",
|
||||
"controle : la ponctuelle rend un rapport centre/bord de {:.2} — si ce rapport monte, la jambe comparative ne compare plus rien",
|
||||
(centre_p as f64 / (bord_p.max(1) as f64))
|
||||
);
|
||||
|
||||
@@ -4162,7 +4161,10 @@ fn le_spot_eclaire_un_faisceau_directe_et_non_un_pied_de_lumiere() {
|
||||
// quasi nulle. Les deux bornes de calibrage (12.15) : **19** propre, **6** quand le facteur de cône
|
||||
// devient un seuil dur (`select(0.0, 1.0, cone >= cos_inner)` — campagne `mutate-spot.py`, M2).
|
||||
let bande_cone = bande(&cone);
|
||||
assert!(bande_cone > 10, "la penombre du cone ne compte que {bande_cone} texels intermediaires (mesure 36) : le bord est devenu un anneau dur — la phrase « a smooth penumbra at the cone's edge » est fausse");
|
||||
assert!(
|
||||
bande_cone > 10,
|
||||
"la penombre du cone ne compte que {bande_cone} texels intermediaires (mesure 36) : le bord est devenu un anneau dur — la phrase « a smooth penumbra at the cone's edge » est fausse"
|
||||
);
|
||||
|
||||
// ── P3 : la tache reste fixe dans le monde quand le corps tourne. Comparaison des deux rendus
|
||||
// (cube a 0 degre puis a 45 degres) : rien ne change dans la bande de sol basse, quelque chose
|
||||
@@ -4182,8 +4184,14 @@ fn le_spot_eclaire_un_faisceau_directe_et_non_un_pied_de_lumiere() {
|
||||
}
|
||||
}
|
||||
eprintln!("15.5 spot : sol bas different={bas} haut different={haut}");
|
||||
assert_eq!(bas, 0, "la rotation du corps change {bas} texels du sol pres de la camera : la tache eclairee suit l'objet au lieu de rester un volume fixe du monde");
|
||||
assert!(haut > 20, "la rotation du corps ne change rien dans la moitie haute ({haut} texels) : la scene de controle ne tourne pas, l'egalite ci-dessus ne prouverait rien");
|
||||
assert_eq!(
|
||||
bas, 0,
|
||||
"la rotation du corps change {bas} texels du sol pres de la camera : la tache eclairee suit l'objet au lieu de rester un volume fixe du monde"
|
||||
);
|
||||
assert!(
|
||||
haut > 20,
|
||||
"la rotation du corps ne change rien dans la moitie haute ({haut} texels) : la scene de controle ne tourne pas, l'egalite ci-dessus ne prouverait rien"
|
||||
);
|
||||
}
|
||||
|
||||
/// Verrou 15.5 — le bandeau de l'exemple `instances` affirme deux choses sur le **bascule**
|
||||
@@ -4213,7 +4221,9 @@ fn basculer_entites_contre_lot_ne_change_que_le_nombre_de_draws() {
|
||||
1,
|
||||
);
|
||||
scene.add_material_shader("mat", "standard").unwrap();
|
||||
scene.set_material_color("mat", [0.0, 0.0, 1.0, 1.0]).unwrap();
|
||||
scene
|
||||
.set_material_color("mat", [0.0, 0.0, 1.0, 1.0])
|
||||
.unwrap();
|
||||
scene
|
||||
.create_mesh("cube", crate::mesh::primitives::cube(0.4), Some("mat"))
|
||||
.unwrap();
|
||||
@@ -4227,7 +4237,9 @@ fn basculer_entites_contre_lot_ne_change_que_le_nombre_de_draws() {
|
||||
})
|
||||
.collect();
|
||||
if lot {
|
||||
scene.add_instance_batch("grid", "cube", &transforms).unwrap();
|
||||
scene
|
||||
.add_instance_batch("grid", "cube", &transforms)
|
||||
.unwrap();
|
||||
} else {
|
||||
for (i, tf) in transforms.iter().enumerate() {
|
||||
scene
|
||||
@@ -4301,19 +4313,37 @@ fn basculer_entites_contre_lot_ne_change_que_le_nombre_de_draws() {
|
||||
);
|
||||
|
||||
// Le nombre de draws EST la revendication : un lot = un draw, N entites = N draws.
|
||||
assert_eq!(draws_lot, 1, "le lot instancie emet {} draws, le bandeau en promet un seul", draws_lot);
|
||||
assert!(draws_entites as usize >= N, "la voie entites emet {draws_entites} draws pour {N} entites : le comparant du bandeau (« ~N ») ne tient plus");
|
||||
assert_eq!(
|
||||
draws_lot, 1,
|
||||
"le lot instancie emet {} draws, le bandeau en promet un seul",
|
||||
draws_lot
|
||||
);
|
||||
assert!(
|
||||
draws_entites as usize >= N,
|
||||
"la voie entites emet {draws_entites} draws pour {N} entites : le comparant du bandeau (« ~N ») ne tient plus"
|
||||
);
|
||||
|
||||
// Les deux voies dessinent : sans ce controle, « seul le nombre de draws change » serait
|
||||
// egalement vrai si une des deux voies ne dessinait rien (gotcha 6.6 / 12.15).
|
||||
assert!(vus(&pixels_lot) > 500, "la voie instanciee ne peint que {} texels hors fond : la scene n'est pas mesurable", vus(&pixels_lot));
|
||||
assert!(vus(&pixels_entites) > 500, "la voie entites ne peint que {} texels hors fond", vus(&pixels_entites));
|
||||
assert!(
|
||||
vus(&pixels_lot) > 500,
|
||||
"la voie instanciee ne peint que {} texels hors fond : la scene n'est pas mesurable",
|
||||
vus(&pixels_lot)
|
||||
);
|
||||
assert!(
|
||||
vus(&pixels_entites) > 500,
|
||||
"la voie entites ne peint que {} texels hors fond",
|
||||
vus(&pixels_entites)
|
||||
);
|
||||
|
||||
// Mesure 2026-11-03 : les deux voies rendent la MEME image, **octet pour octet** — 19 200
|
||||
// texels identiques, 0 different, sur une scene de 11 260 texels hors fond. L'identite est plus
|
||||
// forte qu'un seuil (13.1) : elle dit que l'instancing ne change rien au rendu, qui est
|
||||
// exactement la promesse du bandeau.
|
||||
assert_eq!(ecart, 0, "les deux voies different sur {ecart} texels : « Press L and nothing about the picture changes » est faux — l'instancing a change le rendu, pas seulement l'ordonnancement");
|
||||
assert_eq!(
|
||||
ecart, 0,
|
||||
"les deux voies different sur {ecart} texels : « Press L and nothing about the picture changes » est faux — l'instancing a change le rendu, pas seulement l'ordonnancement"
|
||||
);
|
||||
}
|
||||
|
||||
fn f04_scene(context: &crate::core::Context, transparent_first: bool) -> crate::scene::Scene {
|
||||
|
||||
@@ -366,7 +366,9 @@ fn le_halo_de_bloom_deborde_l_empreinte_du_solide_et_n_y_est_pas_confine() {
|
||||
}
|
||||
let hors = eclaires
|
||||
.iter()
|
||||
.filter(|(x, y)| *x < empreinte.0 || *x > empreinte.2 || *y < empreinte.1 || *y > empreinte.3)
|
||||
.filter(|(x, y)| {
|
||||
*x < empreinte.0 || *x > empreinte.2 || *y < empreinte.1 || *y > empreinte.3
|
||||
})
|
||||
.count();
|
||||
eprintln!(
|
||||
"15.5 bloom halo : max(sans)={max_sans} empreinte={empreinte:?} eclaires={} hors_empreinte={} \
|
||||
|
||||
@@ -73,8 +73,14 @@ fn un_glissement_produit_une_entree_puis_une_sortie_sur_les_deux_predicats() {
|
||||
large.iter().any(|(_, l, _)| !l) && large.iter().any(|(_, l, _)| *l),
|
||||
"le balayage ne produit jamais de changement d'état : `collides` ne dépend pas de la position, ou la scène est mal construite"
|
||||
);
|
||||
assert_eq!(n_large, 2, "le prédicat de boîte change d'état {n_large} fois au lieu de 2 (entrée puis sortie) sur un aller-retour — le bandeau « events fire on entry AND on exit » est faux");
|
||||
assert_eq!(n_precis, 2, "le prédicat exact change d'état {n_precis} fois au lieu de 2");
|
||||
assert_eq!(
|
||||
n_large, 2,
|
||||
"le prédicat de boîte change d'état {n_large} fois au lieu de 2 (entrée puis sortie) sur un aller-retour — le bandeau « events fire on entry AND on exit » est faux"
|
||||
);
|
||||
assert_eq!(
|
||||
n_precis, 2,
|
||||
"le prédicat exact change d'état {n_precis} fois au lieu de 2"
|
||||
);
|
||||
assert_eq!(
|
||||
n_large, n_precis,
|
||||
"les deux prédicats ne basculent pas au même nombre de fois ({n_large} contre {n_precis})"
|
||||
|
||||
@@ -43,12 +43,13 @@ fn une_geometrie_remise_a_jour_deplace_le_raycast_et_la_boite() {
|
||||
.add_entity_with_transform("blob", "blob", crate::core::Transform::identity())
|
||||
.unwrap();
|
||||
|
||||
|
||||
// État initial : rayon 1 → la sonde à −1,2 entre à −1 (distance 0,2), celle à −1,5 ne touche rien.
|
||||
// État initial, rayon 1. Deux sondes, parce qu'un rayon visant la sphère la touche toujours :
|
||||
// ce qui départage les deux rayons est une sonde **décalée** à y = 1,2 (hors du disque de
|
||||
// rayon 1, dans celui de rayon 2) et une sonde d'axe dont la distance d'entrée change.
|
||||
let axe = sonde(&scene, -3.0, 0.0).expect("la sonde d'axe ne touche pas la sphère de rayon 1 : la scène de contrôle ne rayçable pas");
|
||||
let axe = sonde(&scene, -3.0, 0.0).expect(
|
||||
"la sonde d'axe ne touche pas la sphère de rayon 1 : la scène de contrôle ne rayçable pas",
|
||||
);
|
||||
assert!(
|
||||
(axe.distance - 2.0).abs() < 1e-3,
|
||||
"distance d'entrée initiale = {} au lieu de 2,0 (x de −3 à −1)",
|
||||
@@ -72,7 +73,8 @@ fn une_geometrie_remise_a_jour_deplace_le_raycast_et_la_boite() {
|
||||
// l'ancienne forme. Mesure AVANT correction : entree d'axe inchangee a 1,9999999 (au lieu de 1,0)
|
||||
// et sonde a y = 1,2 muette. Apres correction (geometrie CPU derriere un `RwLock`) :
|
||||
// entree d'axe a 1,0 et sonde decomree a 1,4.
|
||||
let apres = sonde(&scene, -3.0, 0.0).expect("après déformation, la sonde d'axe ne touche plus rien");
|
||||
let apres =
|
||||
sonde(&scene, -3.0, 0.0).expect("après déformation, la sonde d'axe ne touche plus rien");
|
||||
assert!(
|
||||
(apres.distance - 1.0).abs() < 1e-3,
|
||||
"la distance d'entrée après `update_geometry` (voie rapide) vaut {} au lieu de 1,0 : la géométrie CPU ne suit plus la voie rapide — le défaut 15.5 est revenu",
|
||||
@@ -220,8 +222,14 @@ fn la_deformation_uploadee_change_la_forme_affichee() {
|
||||
);
|
||||
|
||||
// Les deux rendus peignent : sinon « la silhouette a grandi » serait vrai de vide a vide.
|
||||
assert!(n_avant > 500, "le premier rendu ne peint que {n_avant} texels : la scene de controle ne rend rien, le verrou est un decor");
|
||||
assert!(n_apres > 500, "le second rendu ne peint que {n_apres} texels");
|
||||
assert!(
|
||||
n_avant > 500,
|
||||
"le premier rendu ne peint que {n_avant} texels : la scene de controle ne rend rien, le verrou est un decor"
|
||||
);
|
||||
assert!(
|
||||
n_apres > 500,
|
||||
"le second rendu ne peint que {n_apres} texels"
|
||||
);
|
||||
|
||||
// La voie rapide (meme compte de sommets) doit atteindre le GPU. Deux bornes mesurées
|
||||
// 2026-11-03, cible 160x120, camera a (0,0,3) FOV 90, sphere 8x8 sans BVH : silhouette
|
||||
@@ -255,22 +263,31 @@ fn la_voie_lente_update_geometry_deplace_le_raycast() {
|
||||
);
|
||||
scene.add_material_shader("mat", "standard").unwrap();
|
||||
scene
|
||||
.create_mesh("blob", crate::mesh::primitives::uv_sphere(1.0, 12, 16), Some("mat"))
|
||||
.create_mesh(
|
||||
"blob",
|
||||
crate::mesh::primitives::uv_sphere(1.0, 12, 16),
|
||||
Some("mat"),
|
||||
)
|
||||
.unwrap();
|
||||
scene
|
||||
.add_entity_with_transform("blob", "blob", crate::core::Transform::identity())
|
||||
.unwrap();
|
||||
let avant = scene.raycast_max(glam::Vec3::new(-3.0, 0.0, 0.0), glam::Vec3::X, 20.0);
|
||||
assert!(
|
||||
avant.as_ref().is_some_and(|h| (h.distance - 2.0).abs() < 1e-3),
|
||||
"contrôle : avant la voie lente, entrée d'axe = {:?} au lieu de 2,0", avant.as_ref().map(|h| h.distance)
|
||||
avant
|
||||
.as_ref()
|
||||
.is_some_and(|h| (h.distance - 2.0).abs() < 1e-3),
|
||||
"contrôle : avant la voie lente, entrée d'axe = {:?} au lieu de 2,0",
|
||||
avant.as_ref().map(|h| h.distance)
|
||||
);
|
||||
scene
|
||||
.update_geometry("blob", &crate::mesh::primitives::uv_sphere(2.0, 20, 24))
|
||||
.expect("update_geometry (voie lente) a echoue");
|
||||
let apres = scene.raycast_max(glam::Vec3::new(-3.0, 0.0, 0.0), glam::Vec3::X, 20.0);
|
||||
assert!(
|
||||
apres.as_ref().is_some_and(|h| (h.distance - 1.0).abs() < 1e-3),
|
||||
apres
|
||||
.as_ref()
|
||||
.is_some_and(|h| (h.distance - 1.0).abs() < 1e-3),
|
||||
"voie lente : entrée d'axe = {:?} au lieu de 1,0 — le raycast ne voit pas la nouvelle géométrie",
|
||||
apres.as_ref().map(|h| h.distance)
|
||||
);
|
||||
@@ -296,25 +313,36 @@ fn la_voie_rapide_sans_bvh_deplace_le_raycast() {
|
||||
);
|
||||
scene.add_material_shader("mat", "standard").unwrap();
|
||||
scene
|
||||
.create_mesh("petit", crate::mesh::primitives::uv_sphere(1.0, 8, 8), Some("mat"))
|
||||
.create_mesh(
|
||||
"petit",
|
||||
crate::mesh::primitives::uv_sphere(1.0, 8, 8),
|
||||
Some("mat"),
|
||||
)
|
||||
.unwrap();
|
||||
scene
|
||||
.add_entity_with_transform("petit", "petit", crate::core::Transform::identity())
|
||||
.unwrap();
|
||||
|
||||
// Temoin : ce maillage n'a pas de BVH, donc il emprunte bien la voie rapide.
|
||||
let idx = scene.mesh_index_of("petit").expect("mesh_index_of('petit')");
|
||||
let idx = scene
|
||||
.mesh_index_of("petit")
|
||||
.expect("mesh_index_of('petit')");
|
||||
assert!(
|
||||
scene.mesh_by_index(idx).bvh().is_none(),
|
||||
"temoin faux : la sphere 8x8 a un BVH, le test ne traverse plus la voie rapide"
|
||||
);
|
||||
|
||||
let avant = sonde(&scene, -3.0, 0.0).expect("controle : la sonde d'axe ne touche pas la sphere de rayon 1");
|
||||
let avant = sonde(&scene, -3.0, 0.0)
|
||||
.expect("controle : la sonde d'axe ne touche pas la sphere de rayon 1");
|
||||
assert!(
|
||||
(avant.distance - 2.0).abs() < 1e-3,
|
||||
"controle : entree d'axe = {} au lieu de 2,0", avant.distance
|
||||
"controle : entree d'axe = {} au lieu de 2,0",
|
||||
avant.distance
|
||||
);
|
||||
assert!(
|
||||
sonde(&scene, -3.0, 1.2).is_none(),
|
||||
"controle : la sonde a y = 1,2 touche la sphere de rayon 1"
|
||||
);
|
||||
assert!(sonde(&scene, -3.0, 1.2).is_none(), "controle : la sonde a y = 1,2 touche la sphere de rayon 1");
|
||||
|
||||
scene
|
||||
.update_geometry("petit", &crate::mesh::primitives::uv_sphere(2.0, 8, 8))
|
||||
|
||||
+12
-4
@@ -148,7 +148,9 @@ impl DoFPipeline {
|
||||
sample_count: 1,
|
||||
dimension: wgpu::TextureDimension::D2,
|
||||
format: wgpu::TextureFormat::R16Float,
|
||||
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_SRC,
|
||||
usage: wgpu::TextureUsages::RENDER_ATTACHMENT
|
||||
| wgpu::TextureUsages::TEXTURE_BINDING
|
||||
| wgpu::TextureUsages::COPY_SRC,
|
||||
view_formats: &[],
|
||||
});
|
||||
let coc_view = coc_texture.create_view(&Default::default());
|
||||
@@ -165,7 +167,9 @@ impl DoFPipeline {
|
||||
sample_count: 1,
|
||||
dimension: wgpu::TextureDimension::D2,
|
||||
format: wgpu::TextureFormat::Rgba16Float,
|
||||
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_SRC,
|
||||
usage: wgpu::TextureUsages::RENDER_ATTACHMENT
|
||||
| wgpu::TextureUsages::TEXTURE_BINDING
|
||||
| wgpu::TextureUsages::COPY_SRC,
|
||||
view_formats: &[],
|
||||
});
|
||||
let output_view = output_texture.create_view(&Default::default());
|
||||
@@ -441,7 +445,9 @@ impl DoFPipeline {
|
||||
sample_count: 1,
|
||||
dimension: wgpu::TextureDimension::D2,
|
||||
format: wgpu::TextureFormat::R16Float,
|
||||
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_SRC,
|
||||
usage: wgpu::TextureUsages::RENDER_ATTACHMENT
|
||||
| wgpu::TextureUsages::TEXTURE_BINDING
|
||||
| wgpu::TextureUsages::COPY_SRC,
|
||||
view_formats: &[],
|
||||
});
|
||||
let coc_view = coc_texture.create_view(&Default::default());
|
||||
@@ -457,7 +463,9 @@ impl DoFPipeline {
|
||||
sample_count: 1,
|
||||
dimension: wgpu::TextureDimension::D2,
|
||||
format: wgpu::TextureFormat::Rgba16Float,
|
||||
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_SRC,
|
||||
usage: wgpu::TextureUsages::RENDER_ATTACHMENT
|
||||
| wgpu::TextureUsages::TEXTURE_BINDING
|
||||
| wgpu::TextureUsages::COPY_SRC,
|
||||
view_formats: &[],
|
||||
});
|
||||
let output_view = output_texture.create_view(&Default::default());
|
||||
|
||||
+83
-31
@@ -20,7 +20,12 @@ fn lum(p: &[u8]) -> i32 {
|
||||
/// `dof` = distance de foyer (`None` = DoF éteint) ; `ap` = ouverture. Le rayon max reste 12, comme
|
||||
/// l'exemple — l'ouverture est le bouton qui fait passer l'effet du régime « 2,5 texels » (mesuré,
|
||||
/// invisible à l'empreinte seuillée) au régime où `max_blur` est exploité.
|
||||
fn render_full(dof: Option<f32>, objet: bool, ap: f32, label: &str) -> (Vec<u8>, Vec<f32>, Vec<f32>) {
|
||||
fn render_full(
|
||||
dof: Option<f32>,
|
||||
objet: bool,
|
||||
ap: f32,
|
||||
label: &str,
|
||||
) -> (Vec<u8>, Vec<f32>, Vec<f32>) {
|
||||
let Ok(context) = pollster::block_on(crate::core::Context::new_headless()) else {
|
||||
eprintln!("skip : aucun GPU headless pour le test dof");
|
||||
return (Vec::new(), Vec::new(), Vec::new());
|
||||
@@ -68,11 +73,15 @@ fn render_full(dof: Option<f32>, objet: bool, ap: f32, label: &str) -> (Vec<u8>,
|
||||
.add_material(
|
||||
"mat",
|
||||
std::sync::Arc::new(
|
||||
crate::resources::Material::new(format_scene, "standard", &mut crate::pipeline::PipelineCache::new(
|
||||
std::sync::Arc::new(device.clone()),
|
||||
queue.clone(),
|
||||
1,
|
||||
))
|
||||
crate::resources::Material::new(
|
||||
format_scene,
|
||||
"standard",
|
||||
&mut crate::pipeline::PipelineCache::new(
|
||||
std::sync::Arc::new(device.clone()),
|
||||
queue.clone(),
|
||||
1,
|
||||
),
|
||||
)
|
||||
.with_color([0.9, 0.9, 0.9, 1.0]),
|
||||
),
|
||||
)
|
||||
@@ -118,7 +127,8 @@ fn render_full(dof: Option<f32>, objet: bool, ap: f32, label: &str) -> (Vec<u8>,
|
||||
// texture ne répondent à « le flou est-il appliqué ? » : seule la **différence** des deux le fait.
|
||||
if dof.is_some() && !sortie.is_empty() {
|
||||
if let Some(entree) = renderer.hdr_color_texture_for_test() {
|
||||
let brut_e = crate::core::testutil::readback_texture_u8(&device, &queue, entree, W, H, 8);
|
||||
let brut_e =
|
||||
crate::core::testutil::readback_texture_u8(&device, &queue, entree, W, H, 8);
|
||||
let e = decode_rgba16(&brut_e);
|
||||
let n = sortie.len().min(e.len());
|
||||
let mut s = 0f64;
|
||||
@@ -188,22 +198,19 @@ fn decode_rgba16(brut: &[u8]) -> Vec<f32> {
|
||||
let v = if exp == 0 {
|
||||
mant / 1024.0 * 2f32.powi(-14)
|
||||
} else if exp == 31 {
|
||||
if mant == 0.0 {
|
||||
f32::INFINITY
|
||||
} else {
|
||||
f32::NAN
|
||||
}
|
||||
if mant == 0.0 { f32::INFINITY } else { f32::NAN }
|
||||
} else {
|
||||
(1.0 + mant / 1024.0) * 2f32.powi(exp - 15)
|
||||
};
|
||||
if (f >> 15) & 1 == 1 {
|
||||
-v
|
||||
} else {
|
||||
v
|
||||
}
|
||||
if (f >> 15) & 1 == 1 { -v } else { v }
|
||||
}
|
||||
brut.chunks_exact(8)
|
||||
.map(|c| (h(u16::from_le_bytes([c[0], c[1]])) + h(u16::from_le_bytes([c[2], c[3]])) + h(u16::from_le_bytes([c[4], c[5]]))) / 3.0)
|
||||
.map(|c| {
|
||||
(h(u16::from_le_bytes([c[0], c[1]]))
|
||||
+ h(u16::from_le_bytes([c[2], c[3]]))
|
||||
+ h(u16::from_le_bytes([c[4], c[5]])))
|
||||
/ 3.0
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
@@ -237,8 +244,18 @@ fn empreinte(image: &[u8], fond: &[u8]) -> Empreinte {
|
||||
}
|
||||
}
|
||||
}
|
||||
assert_ne!(x0, u32::MAX, "rien ne diffère du fond : la scène ne dessine pas (contrôle positif, 13.1)");
|
||||
Empreinte { x0, y0, x1, y1, touches }
|
||||
assert_ne!(
|
||||
x0,
|
||||
u32::MAX,
|
||||
"rien ne diffère du fond : la scène ne dessine pas (contrôle positif, 13.1)"
|
||||
);
|
||||
Empreinte {
|
||||
x0,
|
||||
y0,
|
||||
x1,
|
||||
y1,
|
||||
touches,
|
||||
}
|
||||
}
|
||||
|
||||
fn nettete(image: &[u8], b: &Empreinte) -> f64 {
|
||||
@@ -349,7 +366,9 @@ fn le_flou_interieur_est_degrade_et_ne_deborde_pas_de_la_silhouette() {
|
||||
let cx = (e.x0 + e.x1) / 2;
|
||||
let cy = (e.y0 + e.y1) / 2;
|
||||
let demi = ((e.x1 - e.x0) / 2) as usize;
|
||||
let l = |img: &[u8], x: u32, y: u32| lum(&img[((y * W + x) as usize) * 4..((y * W + x) as usize) * 4 + 3]);
|
||||
let l = |img: &[u8], x: u32, y: u32| {
|
||||
lum(&img[((y * W + x) as usize) * 4..((y * W + x) as usize) * 4 + 3])
|
||||
};
|
||||
let fond_l = l(&fond, cx, cy);
|
||||
let inter = l(&net, cx, cy);
|
||||
assert!(
|
||||
@@ -453,8 +472,14 @@ fn le_flou_interieur_est_degrade_et_ne_deborde_pas_de_la_silhouette() {
|
||||
"seulement {dedans} texels adoucis a l interieur de la silhouette (mesure 902) : la scene \
|
||||
ou le noyau a change — controler avant d interpreter le reste"
|
||||
);
|
||||
assert_eq!(bande, 0, "le flou déborde désormais de {bande} texels hors de la silhouette : le noyau mélange l objet et son fond — ce test de caractérisation doit être REMPLACÉ par le verrou de débordement (le bokeh circulaire promis par le bandeau devient vérifiable)");
|
||||
assert_eq!(dehors, 0, "le flou atteint {dehors} texels loin de la silhouette : même motif, remplacer par le verrou de débordement");
|
||||
assert_eq!(
|
||||
bande, 0,
|
||||
"le flou déborde désormais de {bande} texels hors de la silhouette : le noyau mélange l objet et son fond — ce test de caractérisation doit être REMPLACÉ par le verrou de débordement (le bokeh circulaire promis par le bandeau devient vérifiable)"
|
||||
);
|
||||
assert_eq!(
|
||||
dehors, 0,
|
||||
"le flou atteint {dehors} texels loin de la silhouette : même motif, remplacer par le verrou de débordement"
|
||||
);
|
||||
}
|
||||
eprintln!(
|
||||
"15.5 presentation : ecart_ldr_moyen={:.4} ecart_ldr_max={mx} — rampe net=0 flou={}",
|
||||
@@ -471,7 +496,13 @@ fn la_sortie_du_passe_flou_porte_ou_non_le_flou_attendu() {
|
||||
if pixels.is_empty() {
|
||||
return;
|
||||
}
|
||||
assert_eq!(sortie.len(), (W * H) as usize, "la relecture de la sortie du flou n'a pas rendu un valeurs par texel ({} pour {})", sortie.len(), W * H);
|
||||
assert_eq!(
|
||||
sortie.len(),
|
||||
(W * H) as usize,
|
||||
"la relecture de la sortie du flou n'a pas rendu un valeurs par texel ({} pour {})",
|
||||
sortie.len(),
|
||||
W * H
|
||||
);
|
||||
let e = empreinte(&pixels, &fond);
|
||||
let grad = |img: &[f32]| -> f64 {
|
||||
let (mut s, mut n) = (0f64, 0f64);
|
||||
@@ -508,7 +539,13 @@ fn la_sonde_de_cercle_de_confusion_sait_dire_ciel_et_premier_plan() {
|
||||
if pixels.is_empty() {
|
||||
return;
|
||||
}
|
||||
assert_eq!(coc.len(), (W * H) as usize, "la relecture du CoC n'a pas rendu un valeurs par texel ({} lu pour {}) ", coc.len(), W * H);
|
||||
assert_eq!(
|
||||
coc.len(),
|
||||
(W * H) as usize,
|
||||
"la relecture du CoC n'a pas rendu un valeurs par texel ({} lu pour {}) ",
|
||||
coc.len(),
|
||||
W * H
|
||||
);
|
||||
let e = empreinte(&pixels, &fond);
|
||||
let cx = (e.x0 + e.x1) / 2;
|
||||
let cy = (e.y0 + e.y1) / 2;
|
||||
@@ -522,7 +559,10 @@ fn la_sonde_de_cercle_de_confusion_sait_dire_ciel_et_premier_plan() {
|
||||
W * H
|
||||
);
|
||||
|
||||
assert!(ciel.abs() < 0.01, "le texel de ciel a un CoC de {ciel:.4} au lieu de 0 : le shader ne reconnait pas le fond, la sonde ne peut pas servir de controle");
|
||||
assert!(
|
||||
ciel.abs() < 0.01,
|
||||
"le texel de ciel a un CoC de {ciel:.4} au lieu de 0 : le shader ne reconnait pas le fond, la sonde ne peut pas servir de controle"
|
||||
);
|
||||
assert!(
|
||||
centre > 0.01,
|
||||
"le centre du cube hors foyer a un CoC de {centre:.4} : le cercle de confusion est **nul sur la geometrie** — le defaut est donc en AMONT (dof_coc.wgsl ou l'uniform near/far/log_depth), pas en aval"
|
||||
@@ -545,8 +585,18 @@ fn le_dof_etale_le_meme_objet_et_reduit_sa_nettete() {
|
||||
eprintln!(
|
||||
"15.5 dof : net boite=({},{})-({},{}) lx={lx_net} ly={} touches={} nettete={n_net:.2} ; \
|
||||
flou boite=({},{})-({},{}) lx={lx_flou} ly={} touches={} nettete={n_flou:.2}",
|
||||
net.x0, net.y0, net.x1, net.y1, net.y1 - net.y0 + 1, net.touches,
|
||||
flou.x0, flou.y0, flou.x1, flou.y1, flou.y1 - flou.y0 + 1, flou.touches
|
||||
net.x0,
|
||||
net.y0,
|
||||
net.x1,
|
||||
net.y1,
|
||||
net.y1 - net.y0 + 1,
|
||||
net.touches,
|
||||
flou.x0,
|
||||
flou.y0,
|
||||
flou.x1,
|
||||
flou.y1,
|
||||
flou.y1 - flou.y0 + 1,
|
||||
flou.touches
|
||||
);
|
||||
|
||||
// **Caractérisation** (famille `log_depth_drops_triangles_spanning_a_large_depth_range`) — et son
|
||||
@@ -563,8 +613,7 @@ fn le_dof_etale_le_meme_objet_et_reduit_sa_nettete() {
|
||||
"l'empreinte avec DoF ({} texels) diffère de celle sans DoF ({}) : le DoF agit désormais — \
|
||||
ce test de caractérisation doit être REMPLACÉ par le verrou d'étalement, et la promesse du \
|
||||
bandeau devient confrontable",
|
||||
flou.touches,
|
||||
net.touches
|
||||
flou.touches, net.touches
|
||||
);
|
||||
assert!(
|
||||
(n_flou - n_net).abs() < 0.5,
|
||||
@@ -617,7 +666,10 @@ fn le_surplus_cree_par_le_flou_est_rond_et_pas_carre() {
|
||||
return;
|
||||
}
|
||||
let (lx, ly) = (sx1 - sx0 + 1, sy1 - sy0 + 1);
|
||||
assert!(lx >= 6 && ly >= 6, "la boîte du surplus fait {lx}×{ly} : trop petite pour asserter une forme (13.1)");
|
||||
assert!(
|
||||
lx >= 6 && ly >= 6,
|
||||
"la boîte du surplus fait {lx}×{ly} : trop petite pour asserter une forme (13.1)"
|
||||
);
|
||||
let aire = (lx * ly) as usize;
|
||||
let taux = surplus as f64 / aire as f64;
|
||||
|
||||
|
||||
@@ -54,11 +54,11 @@ fn render(hdr: Option<ToneMapper>, intensite: f32, reference: bool, label: &str)
|
||||
H,
|
||||
&crate::core::shadow::ShadowConfig::default(),
|
||||
hdr,
|
||||
None, // bloom
|
||||
None, // msaa
|
||||
None, // fog
|
||||
None, // dof
|
||||
None, // ssao
|
||||
None, // bloom
|
||||
None, // msaa
|
||||
None, // fog
|
||||
None, // dof
|
||||
None, // ssao
|
||||
false, // log_depth
|
||||
false, // depth_prepass
|
||||
);
|
||||
@@ -84,41 +84,43 @@ fn render(hdr: Option<ToneMapper>, intensite: f32, reference: bool, label: &str)
|
||||
// est MULTIPLIÉ par l'albédo : un albédo noir annule l'émissif, sans erreur ni log. C'est la
|
||||
// famille `ObjectUniform::ambient_color = (0,0,0,0)` (« une valeur par défaut qui annule un terme
|
||||
// entier ne se voit pas ») — et le harnais vient de le trouver, pas la relecture.
|
||||
scene.add_material(
|
||||
"brillant",
|
||||
std::sync::Arc::new(
|
||||
crate::resources::Material::new(
|
||||
format_scene,
|
||||
"standard",
|
||||
&mut crate::pipeline::PipelineCache::new(
|
||||
std::sync::Arc::new(device.clone()),
|
||||
queue.clone(),
|
||||
1,
|
||||
),
|
||||
)
|
||||
.with_color([0.5, 0.5, 0.5, 1.0]),
|
||||
),
|
||||
)
|
||||
.expect("matériau brillant");
|
||||
scene
|
||||
.add_material(
|
||||
"brillant",
|
||||
std::sync::Arc::new(
|
||||
crate::resources::Material::new(
|
||||
format_scene,
|
||||
"standard",
|
||||
&mut crate::pipeline::PipelineCache::new(
|
||||
std::sync::Arc::new(device.clone()),
|
||||
queue.clone(),
|
||||
1,
|
||||
),
|
||||
)
|
||||
.with_color([0.5, 0.5, 0.5, 1.0]),
|
||||
),
|
||||
)
|
||||
.expect("matériau brillant");
|
||||
scene
|
||||
.set_material_emissive("brillant", [1.0, 1.0, 1.0, intensite])
|
||||
.expect("emissif");
|
||||
scene.add_material(
|
||||
"reference",
|
||||
std::sync::Arc::new(
|
||||
crate::resources::Material::new(
|
||||
format_scene,
|
||||
"standard",
|
||||
&mut crate::pipeline::PipelineCache::new(
|
||||
std::sync::Arc::new(device.clone()),
|
||||
queue.clone(),
|
||||
1,
|
||||
),
|
||||
)
|
||||
.with_color([0.5, 0.5, 0.5, 1.0]),
|
||||
),
|
||||
)
|
||||
.expect("matériau référence");
|
||||
scene
|
||||
.add_material(
|
||||
"reference",
|
||||
std::sync::Arc::new(
|
||||
crate::resources::Material::new(
|
||||
format_scene,
|
||||
"standard",
|
||||
&mut crate::pipeline::PipelineCache::new(
|
||||
std::sync::Arc::new(device.clone()),
|
||||
queue.clone(),
|
||||
1,
|
||||
),
|
||||
)
|
||||
.with_color([0.5, 0.5, 0.5, 1.0]),
|
||||
),
|
||||
)
|
||||
.expect("matériau référence");
|
||||
|
||||
scene
|
||||
.create_mesh("bloc", crate::mesh::cube(1.0), Some("brillant"))
|
||||
@@ -173,11 +175,7 @@ fn boite_changes(a: &[u8], b: &[u8], seuil: i32) -> Option<(u32, u32, u32, u32)>
|
||||
}
|
||||
}
|
||||
}
|
||||
if n == 0 {
|
||||
None
|
||||
} else {
|
||||
Some((x0, y0, x1, y1))
|
||||
}
|
||||
if n == 0 { None } else { Some((x0, y0, x1, y1)) }
|
||||
}
|
||||
|
||||
/// Nombre de texels différents à l'intérieur d'une boîte.
|
||||
@@ -185,14 +183,10 @@ fn changes_dans(a: &[u8], b: &[u8], boite: (u32, u32, u32, u32), seuil: i32) ->
|
||||
(0..H)
|
||||
.flat_map(|y| (0..W).map(move |x| (x, y)))
|
||||
.filter(|(x, y)| {
|
||||
*x >= boite.0
|
||||
&& *x <= boite.2
|
||||
&& *y >= boite.1
|
||||
&& *y <= boite.3
|
||||
&& {
|
||||
let i = ((y * W + x) as usize) * 4;
|
||||
(lum(&a[i..i + 4]) - lum(&b[i..i + 4])).abs() >= seuil
|
||||
}
|
||||
*x >= boite.0 && *x <= boite.2 && *y >= boite.1 && *y <= boite.3 && {
|
||||
let i = ((y * W + x) as usize) * 4;
|
||||
(lum(&a[i..i + 4]) - lum(&b[i..i + 4])).abs() >= seuil
|
||||
}
|
||||
})
|
||||
.count()
|
||||
}
|
||||
@@ -218,7 +212,8 @@ fn changes_dans(a: &[u8], b: &[u8], boite: (u32, u32, u32, u32), seuil: i32) ->
|
||||
/// (seuil asserté 300, facteur ~4 de marge) ; `ref_bouge` = **0** ; empreinte de la référence
|
||||
/// `(106,44)-(143,75)` et zone HDR `(16,44)-(53,75)`, disjointes — la scène posée est bien lue.
|
||||
#[test]
|
||||
fn au_dessus_de_un_en_ldr_deux_niveaux_d_emission_rendent_a_l_identique_et_la_reference_ne_bouge_pas() {
|
||||
fn au_dessus_de_un_en_ldr_deux_niveaux_d_emission_rendent_a_l_identique_et_la_reference_ne_bouge_pas()
|
||||
{
|
||||
let (l2, l4, h2, h4, l2_sans_ref) = (
|
||||
render(None, 2.0, true, "ldr 2"),
|
||||
render(None, 4.0, true, "ldr 4"),
|
||||
@@ -230,7 +225,8 @@ fn au_dessus_de_un_en_ldr_deux_niveaux_d_emission_rendent_a_l_identique_et_la_re
|
||||
return; // skip : la ligne `skip :` sur stderr est le discriminant (13.1).
|
||||
}
|
||||
|
||||
let empreinte_ref = boite_changes(&l2, &l2_sans_ref, 8).expect("la référence ne se dessine pas");
|
||||
let empreinte_ref =
|
||||
boite_changes(&l2, &l2_sans_ref, 8).expect("la référence ne se dessine pas");
|
||||
let zone_hdr = boite_changes(&h2, &h4, 8).expect("l'émissif ne change rien en HDR");
|
||||
let changes_ldr = boite_changes(&l2, &l4, 1);
|
||||
let total_ldr: usize = l2
|
||||
|
||||
@@ -102,7 +102,11 @@ fn render(hdr: Option<ToneMapper>, intensite: f32, label: &str) -> Vec<u8> {
|
||||
.expect("matériau référence");
|
||||
|
||||
scene
|
||||
.create_mesh("sphere", crate::mesh::uv_sphere(1.0, 48, 24), Some("brillant"))
|
||||
.create_mesh(
|
||||
"sphere",
|
||||
crate::mesh::uv_sphere(1.0, 48, 24),
|
||||
Some("brillant"),
|
||||
)
|
||||
.expect("maillage sphère");
|
||||
let mut tf = crate::Transform::identity();
|
||||
tf.translation = glam::Vec3::new(-1.5, 0.0, 0.0);
|
||||
@@ -150,7 +154,11 @@ fn empreinte_spheree(a: &[u8], b: &[u8]) -> (u32, u32, u32, u32) {
|
||||
}
|
||||
}
|
||||
}
|
||||
assert_ne!(x0, u32::MAX, "la sphère ne change rien entre émissif 0 et 3 : la scène est vide");
|
||||
assert_ne!(
|
||||
x0,
|
||||
u32::MAX,
|
||||
"la sphère ne change rien entre émissif 0 et 3 : la scène est vide"
|
||||
);
|
||||
(x0, y0, x1, y1)
|
||||
}
|
||||
|
||||
@@ -283,7 +291,10 @@ fn aces_presse_le_pic_plus_tard_que_reinhard_et_le_signe_du_desaccord_ne_s_inver
|
||||
/// amplitude du dégradé HDR est faible — voir le brouillon 15.5, qui cite les deux bornes mesurées.
|
||||
#[test]
|
||||
fn le_pic_ldr_est_un_plateau_et_le_pic_hdr_garde_des_niveaux_distincts() {
|
||||
let (ldr, hdr) = (render(None, 3.0, "ldr plateau"), render(Some(ToneMapper::Aces), 3.0, "hdr pic"));
|
||||
let (ldr, hdr) = (
|
||||
render(None, 3.0, "ldr plateau"),
|
||||
render(Some(ToneMapper::Aces), 3.0, "hdr pic"),
|
||||
);
|
||||
let (l0, l3) = (render(None, 0.0, "ldr 0c"), render(None, 3.0, "ldr 3c"));
|
||||
if ldr.is_empty() {
|
||||
return;
|
||||
|
||||
@@ -54,7 +54,7 @@ fn le_quad_des_exemples_est_tourne_vers_la_camera_par_la_voie_scene() {
|
||||
concat!(env!("CARGO_MANIFEST_DIR"), "/examples/meshes/simple.rs"),
|
||||
concat!(env!("CARGO_MANIFEST_DIR"), "/examples/meshes/manual.rs"),
|
||||
] {
|
||||
let src = std::fs::read_to_string(rel).expect("exemple") ;
|
||||
let src = std::fs::read_to_string(rel).expect("exemple");
|
||||
assert!(
|
||||
src.contains("with_indices(vec![0, 2, 1, 0, 3, 2])"),
|
||||
"{rel} ne porte plus l'ordre d'indices verrouillé ici : le winding du quad des exemples a bougé"
|
||||
@@ -87,7 +87,11 @@ fn le_quad_des_exemples_est_tourne_vers_la_camera_par_la_voie_scene() {
|
||||
renderer.set_unlit(true);
|
||||
let tex = context.device.create_texture(&wgpu::TextureDescriptor {
|
||||
label: Some("sonde-winding"),
|
||||
size: wgpu::Extent3d { width: W, height: H, depth_or_array_layers: 1 },
|
||||
size: wgpu::Extent3d {
|
||||
width: W,
|
||||
height: H,
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: wgpu::TextureDimension::D2,
|
||||
@@ -97,11 +101,17 @@ fn le_quad_des_exemples_est_tourne_vers_la_camera_par_la_voie_scene() {
|
||||
});
|
||||
let view = tex.create_view(&Default::default());
|
||||
renderer.render_scene(&view, &mut scene, W as f32 / H as f32, 1.0, 0.016);
|
||||
let px = crate::core::testutil::readback_texture_rgba8(&context.device, &context.queue, &tex, W, H);
|
||||
let px =
|
||||
crate::core::testutil::readback_texture_rgba8(&context.device, &context.queue, &tex, W, H);
|
||||
let fond = crate::core::testutil::clear_color_bytes();
|
||||
let vu = (0..(W * H) as usize).filter(|i| {
|
||||
px[i * 4..i * 4 + 3].iter().zip(fond.iter()).any(|(a, b)| (*a as i32 - *b as i32).abs() > 8)
|
||||
}).count();
|
||||
let vu = (0..(W * H) as usize)
|
||||
.filter(|i| {
|
||||
px[i * 4..i * 4 + 3]
|
||||
.iter()
|
||||
.zip(fond.iter())
|
||||
.any(|(a, b)| (*a as i32 - *b as i32).abs() > 8)
|
||||
})
|
||||
.count();
|
||||
eprintln!("15.5 winding scene (indices des exemples) : texels peints = {vu}");
|
||||
assert!(
|
||||
vu > 500,
|
||||
@@ -119,8 +129,11 @@ fn la_voie_manuelle_peint_le_quad_a_sa_position_ndc_predite() {
|
||||
const H: u32 = 120;
|
||||
let format = wgpu::TextureFormat::Rgba8Unorm;
|
||||
|
||||
let mut cache =
|
||||
crate::pipeline::PipelineCache::new(Arc::new(context.device.clone()), context.queue.clone(), 1);
|
||||
let mut cache = crate::pipeline::PipelineCache::new(
|
||||
Arc::new(context.device.clone()),
|
||||
context.queue.clone(),
|
||||
1,
|
||||
);
|
||||
let mut renderer = crate::core::Renderer::new(
|
||||
&context,
|
||||
format,
|
||||
@@ -170,13 +183,17 @@ fn la_voie_manuelle_peint_le_quad_a_sa_position_ndc_predite() {
|
||||
let view = tex.create_view(&Default::default());
|
||||
renderer.render(&view, &mesh, &material);
|
||||
|
||||
let px = crate::core::testutil::readback_texture_rgba8(&context.device, &context.queue, &tex, W, H);
|
||||
let px =
|
||||
crate::core::testutil::readback_texture_rgba8(&context.device, &context.queue, &tex, W, H);
|
||||
|
||||
// Le fond est la couleur de clear du renderer, pas la couleur la plus fréquente de l'image
|
||||
// (gotcha 14-F8).
|
||||
let fond = crate::core::testutil::clear_color_bytes();
|
||||
let different_du_fond = |i: usize| -> bool {
|
||||
px[i..i + 3].iter().zip(fond.iter()).any(|(a, b)| (*a as i32 - *b as i32).abs() > 8)
|
||||
px[i..i + 3]
|
||||
.iter()
|
||||
.zip(fond.iter())
|
||||
.any(|(a, b)| (*a as i32 - *b as i32).abs() > 8)
|
||||
};
|
||||
|
||||
let (mut x0, mut y0, mut x1, mut y1) = (W, H, 0u32, 0u32);
|
||||
@@ -224,12 +241,20 @@ fn la_voie_manuelle_peint_le_quad_a_sa_position_ndc_predite() {
|
||||
|
||||
// Une couleur par sommet doit arriver à l'écran : le coin rouge en haut à gauche, bleu en bas à
|
||||
// droite (mesuré, puis épinglé — la prédiction est la table de couleurs de la géométrie).
|
||||
let (rg, vg, bg) = (px[coin_gauche_haut] as i32, px[coin_gauche_haut + 1] as i32, px[coin_gauche_haut + 2] as i32);
|
||||
let (rg, vg, bg) = (
|
||||
px[coin_gauche_haut] as i32,
|
||||
px[coin_gauche_haut + 1] as i32,
|
||||
px[coin_gauche_haut + 2] as i32,
|
||||
);
|
||||
assert!(
|
||||
rg > 150 && vg < 90 && bg < 90,
|
||||
"le coin haut-gauche rend ({rg},{vg},{bg}) au lieu d'un rouge dominant : la couleur par sommet n'atteint plus l'écran par la voie manuelle"
|
||||
);
|
||||
let (rb, vb, bb) = (px[coin_droit_bas] as i32, px[coin_droit_bas + 1] as i32, px[coin_droit_bas + 2] as i32);
|
||||
let (rb, vb, bb) = (
|
||||
px[coin_droit_bas] as i32,
|
||||
px[coin_droit_bas + 1] as i32,
|
||||
px[coin_droit_bas + 2] as i32,
|
||||
);
|
||||
assert!(
|
||||
bb > 150 && rb < 90 && vb < 90,
|
||||
"le coin bas-droit rend ({rb},{vb},{bb}) au lieu d'un bleu dominant : interpolation de couleur brisée sur la voie manuelle"
|
||||
|
||||
+16
-14
@@ -14,18 +14,18 @@
|
||||
#[cfg(test)]
|
||||
mod batch_gpu_tests;
|
||||
|
||||
/// 15.5 — famille « ombres » de la file des bandeaux : pénombre PCF, allongement selon la lumière,
|
||||
/// rectangle en vue de dessus, ombre propre par casteur et bascule `set_shadow_caster`. `cfg(test)` only.
|
||||
#[cfg(test)]
|
||||
mod shadow_edge_tests;
|
||||
#[cfg(test)]
|
||||
mod msaa_tests;
|
||||
mod dof_tests;
|
||||
#[cfg(test)]
|
||||
mod emissive_tests;
|
||||
#[cfg(test)]
|
||||
mod hdr_tests;
|
||||
#[cfg(test)]
|
||||
mod dof_tests;
|
||||
mod msaa_tests;
|
||||
/// 15.5 — famille « ombres » de la file des bandeaux : pénombre PCF, allongement selon la lumière,
|
||||
/// rectangle en vue de dessus, ombre propre par casteur et bascule `set_shadow_caster`. `cfg(test)` only.
|
||||
#[cfg(test)]
|
||||
mod shadow_edge_tests;
|
||||
|
||||
/// Phase 14 — F1 : témoin du défaut de tri intra-lot. `cfg(test)` only.
|
||||
///
|
||||
@@ -37,10 +37,14 @@ mod batch_sort_tests;
|
||||
|
||||
pub mod bloom;
|
||||
pub mod collision;
|
||||
#[cfg(test)]
|
||||
mod collision_sweep_tests;
|
||||
pub mod context;
|
||||
/// 6.6 — Cascaded Shadow Maps : découpe, frusta, calage de texels, sélection. Géométrie pure,
|
||||
/// testable sans GPU — le WGSL n'en est que le port.
|
||||
pub mod csm;
|
||||
#[cfg(test)]
|
||||
mod deform_tests;
|
||||
pub mod dof;
|
||||
pub mod emitter;
|
||||
pub mod fog;
|
||||
@@ -50,25 +54,23 @@ pub mod geometry;
|
||||
pub mod hdr;
|
||||
pub mod lod;
|
||||
pub mod manual;
|
||||
#[cfg(test)]
|
||||
mod manual_path_tests;
|
||||
pub mod msaa;
|
||||
pub mod particles;
|
||||
/// Étape 12.13 : picking par instance (passe d'IDs, readback d'un pixel, machine à états).
|
||||
pub mod pick;
|
||||
#[cfg(test)]
|
||||
mod pick_tests;
|
||||
#[cfg(test)]
|
||||
mod collision_sweep_tests;
|
||||
#[cfg(test)]
|
||||
mod deform_tests;
|
||||
#[cfg(test)]
|
||||
mod manual_path_tests;
|
||||
#[cfg(test)]
|
||||
mod pick_scaled_tests;
|
||||
#[cfg(test)]
|
||||
mod pick_tests;
|
||||
/// Banc P5 piste #1 (`poll(Wait)` contre `poll(Poll)`).
|
||||
/// `#[cfg(test)]` : ce module ne doit jamais entrer dans le binaire de release (verrou
|
||||
/// `every_test_is_masked_from_the_release_build`).
|
||||
#[cfg(test)]
|
||||
mod poll_bench;
|
||||
#[cfg(test)]
|
||||
mod spot_pbr_tests;
|
||||
|
||||
/// Piste #7 — verrous GPU de la chaine de mips (upload + invariant d'image). `cfg(test)`
|
||||
/// uniquement : un test qui ne peut pas entrer dans le binaire de release.
|
||||
|
||||
@@ -49,7 +49,11 @@ fn rendu(context: &Context, az: f32, samples: Option<u32>, label: &str) -> Vec<u
|
||||
|
||||
let d = 4.0f32;
|
||||
let eye = glam::Vec3::new(az.sin() * d, 0.0, az.cos() * d);
|
||||
scene.set_camera(crate::camera::Camera::perspective(eye, glam::Vec3::ZERO, glam::Vec3::Y));
|
||||
scene.set_camera(crate::camera::Camera::perspective(
|
||||
eye,
|
||||
glam::Vec3::ZERO,
|
||||
glam::Vec3::Y,
|
||||
));
|
||||
|
||||
let tex = context.device.create_texture(&wgpu::TextureDescriptor {
|
||||
label: Some(label),
|
||||
@@ -183,7 +187,9 @@ fn msaa_adoucit_le_bord_de_la_silhouette_et_ne_touche_pas_l_interieur() {
|
||||
}
|
||||
}
|
||||
}
|
||||
eprintln!("15.5 msaa interieur : identiques={identiques} ecartes={ecartes} ecart_max={ecart_max:.1}");
|
||||
eprintln!(
|
||||
"15.5 msaa interieur : identiques={identiques} ecartes={ecartes} ecart_max={ecart_max:.1}"
|
||||
);
|
||||
assert!(
|
||||
identiques > 1_000,
|
||||
"seulement {identiques} texels d'interieur compares : scene hors de cause, la jambe \
|
||||
|
||||
@@ -30,11 +30,7 @@ fn une_entite_scalee_est_raycee_a_sa_boite_monde_reelle() {
|
||||
);
|
||||
scene.add_material_shader("mat", "standard").unwrap();
|
||||
scene
|
||||
.create_mesh(
|
||||
"cube",
|
||||
crate::mesh::primitives::cube(1.0),
|
||||
Some("mat"),
|
||||
)
|
||||
.create_mesh("cube", crate::mesh::primitives::cube(1.0), Some("mat"))
|
||||
.unwrap();
|
||||
let mut tf = crate::core::Transform::identity();
|
||||
if haute {
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
//! Chaque scène ci-dessous est d'abord confrontée à un **contrôle positif** — « l'ombre existe »,
|
||||
//! « le sol éclairé domine » — avant toute assertion sur la forme de l'ombre.
|
||||
|
||||
use crate::core::testutil::{bands_below, horizon_row, luma, shadow_bbox_below, GroundBands};
|
||||
use crate::core::testutil::{GroundBands, bands_below, horizon_row, luma, shadow_bbox_below};
|
||||
|
||||
const W: u32 = 320;
|
||||
const H: u32 = 240;
|
||||
@@ -62,14 +62,10 @@ fn scene_with_casters(
|
||||
.unwrap();
|
||||
for (i, (x, y, z)) in casters.iter().enumerate() {
|
||||
let name = format!("caster_{i}");
|
||||
scene
|
||||
.create_mesh(&name, cube(2.0), Some("mat"))
|
||||
.unwrap();
|
||||
scene.create_mesh(&name, cube(2.0), Some("mat")).unwrap();
|
||||
let mut tf = crate::core::Transform::identity();
|
||||
tf.translation = glam::Vec3::new(*x, *y + 1.0, *z);
|
||||
scene
|
||||
.add_entity_with_transform(&name, &name, tf)
|
||||
.unwrap();
|
||||
scene.add_entity_with_transform(&name, &name, tf).unwrap();
|
||||
}
|
||||
scene.clear_lights();
|
||||
scene
|
||||
@@ -168,9 +164,20 @@ fn l_arete_d_ombre_presente_une_penombre() {
|
||||
);
|
||||
// Contrôles d'existence AVANT la forme : une image uniforme donnerait bande = 0 et ferait
|
||||
// passer « aucune pénombre » pour une frontière parfaite.
|
||||
assert!(g.sol < H, "aucun sol dans l'image : le cadre est faux (gotcha 6.6)");
|
||||
assert!(g.sombre > 300, "aucune ombre mesurée ({} px) : la passe d'ombre ne dessine pas", g.sombre);
|
||||
assert!(g.clair > 3_000, "le sol éclairé domine mal ({} px) : la mesure n'est pas calibrable", g.clair);
|
||||
assert!(
|
||||
g.sol < H,
|
||||
"aucun sol dans l'image : le cadre est faux (gotcha 6.6)"
|
||||
);
|
||||
assert!(
|
||||
g.sombre > 300,
|
||||
"aucune ombre mesurée ({} px) : la passe d'ombre ne dessine pas",
|
||||
g.sombre
|
||||
);
|
||||
assert!(
|
||||
g.clair > 3_000,
|
||||
"le sol éclairé domine mal ({} px) : la mesure n'est pas calibrable",
|
||||
g.clair
|
||||
);
|
||||
// Mesuré : bande = 317 px à carte 256². La mutation « PCF réduit à un seul tap » doit faire
|
||||
// s'effondrer cette bande (jouée par `campaigns/mutate-shadows.py`) ; le seuil de 100 est ENTRE
|
||||
// les deux bornes mesurées, pas au-dessus de la valeur propre.
|
||||
@@ -214,7 +221,10 @@ fn l_ombre_s_allonge_dans_le_sens_de_la_lumiere() {
|
||||
&[(-1.0, 0.0, -6.0)],
|
||||
);
|
||||
let y = sol(&rendu(&context, &mut s, 512));
|
||||
assert!(y < H, "la caméra de référence ne montre pas de sol : le cadre est faux (gotcha 6.6)");
|
||||
assert!(
|
||||
y < H,
|
||||
"la caméra de référence ne montre pas de sol : le cadre est faux (gotcha 6.6)"
|
||||
);
|
||||
y
|
||||
};
|
||||
let mut mes: Vec<(&str, u32, u32, f32)> = Vec::new();
|
||||
@@ -263,16 +273,15 @@ fn l_ombre_s_allonge_dans_le_sens_de_la_lumiere() {
|
||||
eprintln!(
|
||||
"15.5 allongement : aire rase={} haute={} | largeur rase={} haute={} | barycentre rase={:.1} \
|
||||
haute={:.1}",
|
||||
rase.1,
|
||||
haute.1,
|
||||
rase.2,
|
||||
haute.2,
|
||||
rase.3,
|
||||
haute.3
|
||||
rase.1, haute.1, rase.2, haute.2, rase.3, haute.3
|
||||
);
|
||||
// Contrôle positif dimensionné (gotcha 13.1 : « non nul » ne dit pas que la scène est mesurable).
|
||||
assert!(haute.1 > 400, "jambe à lumière haute sans ombre visible ({}) : la scène ne montre pas ce \
|
||||
qu'elle est censée mesurer", haute.1);
|
||||
assert!(
|
||||
haute.1 > 400,
|
||||
"jambe à lumière haute sans ombre visible ({}) : la scène ne montre pas ce \
|
||||
qu'elle est censée mesurer",
|
||||
haute.1
|
||||
);
|
||||
assert!(
|
||||
rase.1 > 2 * haute.1,
|
||||
"la lumière rase ne rallonge pas l'ombre : aire {} contre {} à lumière haute",
|
||||
@@ -338,8 +347,16 @@ fn l_ombre_d_un_cube_leve_s_ecarte_et_celle_d_un_cube_pose_la_rejoint() {
|
||||
leve.1,
|
||||
leve.1 - pose.1
|
||||
);
|
||||
assert!(pose.0 > 200, "aucune empreinte pour le cube posé ({} px)", pose.0);
|
||||
assert!(leve.0 > 200, "aucune empreinte pour le cube levé ({} px)", leve.0);
|
||||
assert!(
|
||||
pose.0 > 200,
|
||||
"aucune empreinte pour le cube posé ({} px)",
|
||||
pose.0
|
||||
);
|
||||
assert!(
|
||||
leve.0 > 200,
|
||||
"aucune empreinte pour le cube levé ({} px)",
|
||||
leve.0
|
||||
);
|
||||
assert!(
|
||||
leve.1 - pose.1 > 5.0,
|
||||
"lever le cube de 2,5 unités ne déplace pas son ombre : barycentre {:.1} contre {:.1} — une \
|
||||
@@ -376,16 +393,17 @@ fn l_ombre_vue_de_dessus_est_un_rectangle() {
|
||||
let y0 = 0u32;
|
||||
let g = bands(&img, y0);
|
||||
let (bb, n) = shadow_bbox_below(&img, W, H, y0, g.eclaire * 35 / 100);
|
||||
let remplissage = bb.map(|(x0, x1, y0b, y1b)| {
|
||||
n as f32 / ((x1 - x0 + 1) as f32 * (y1b - y0b + 1) as f32)
|
||||
});
|
||||
let remplissage =
|
||||
bb.map(|(x0, x1, y0b, y1b)| n as f32 / ((x1 - x0 + 1) as f32 * (y1b - y0b + 1) as f32));
|
||||
eprintln!(
|
||||
"15.5 vue de dessus : sol={y0} éclaire={} sombre={n} bbox={bb:?} remplissage={remplissage:?} \
|
||||
bande={}",
|
||||
g.eclaire,
|
||||
g.bande
|
||||
g.eclaire, g.bande
|
||||
);
|
||||
assert!(
|
||||
n > 300,
|
||||
"aucune ombre en vue de dessus ({n} px) : rien à mesurer"
|
||||
);
|
||||
assert!(n > 300, "aucune ombre en vue de dessus ({n} px) : rien à mesurer");
|
||||
let r = remplissage.expect("au moins un pixel sous le seuil");
|
||||
// Mesuré : 0,985. Seuil à 0,80 : un flou appliqué à l'ombre (ou un PCF à grand rayon) creuse les
|
||||
// angles et ferait tomber ce taux bien en dessous.
|
||||
@@ -471,16 +489,13 @@ fn chaque_caster_ecrit_sa_propre_ombre_et_le_retrait_eteint_tout() {
|
||||
"15.5 trois casteurs : sol={y0} éclaire={} sombre={n} bbox={bb:?} régions={:?} étendue={} \
|
||||
plus_large={} | sans caster : sombre={} bande_on={} bande_off={} | diff_sol={diff_sol} \
|
||||
diff_ciel={diff_ciel}",
|
||||
g.eclaire,
|
||||
largeurs,
|
||||
etendue,
|
||||
plus_large,
|
||||
goff.sombre,
|
||||
g.bande,
|
||||
goff.bande,
|
||||
g.eclaire, largeurs, etendue, plus_large, goff.sombre, g.bande, goff.bande,
|
||||
);
|
||||
|
||||
assert!(y0 < H, "aucun sol dans l'image : le cadre est faux (gotcha 6.6)");
|
||||
assert!(
|
||||
y0 < H,
|
||||
"aucun sol dans l'image : le cadre est faux (gotcha 6.6)"
|
||||
);
|
||||
assert_eq!(
|
||||
largeurs.len(),
|
||||
3,
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
//! Verrou 15.5 — l'entrée fragment **`fs_pbr`** sous spot n'était traversée par **aucun** test.
|
||||
//! Le faisceau est calculé à DEUX endroits du shader standard (`fs_main` et `fs_pbr`, famille 6.7
|
||||
//! « brancher dans TOUS les points d'entrée ») ; le verrou `le_spot_eclaire_un_faisceau_directe_et_non_un_pied_de_lumiere`
|
||||
//! construit son matériau avec `add_material_shader`, donc ne rend que `fs_main`. Retirer le bloc cône
|
||||
//! de `fs_pbr` ne faisait rougir **aucune** suite — mesuré par la campagne `mutate-spot.py`, qui a du
|
||||
//! refuser de conclure sur l'ancre à deux occurrences.
|
||||
//!
|
||||
//! Ce verrou rend la **même scène** avec un matériau **PBR** (donc `fs_pbr`) et répète le discriminant
|
||||
//! directif : rapport centre/bord sous cône, contre le même rapport sous lumière ponctuelle.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
#[test]
|
||||
fn le_spot_eclaire_aussi_en_pbr_un_faisceau_et_non_un_pied_de_lumiere() {
|
||||
let Ok(context) = pollster::block_on(crate::core::Context::new_headless()) else {
|
||||
eprintln!("skip : pas de GPU headless");
|
||||
return;
|
||||
};
|
||||
const W: u32 = 200;
|
||||
const H: u32 = 150;
|
||||
|
||||
let construis = |outer: f32| -> crate::scene::Scene {
|
||||
let mut scene = crate::scene::Scene::new();
|
||||
scene.init_gpu(
|
||||
Arc::new(context.device.clone()),
|
||||
context.queue.clone(),
|
||||
wgpu::TextureFormat::Rgba8Unorm,
|
||||
1,
|
||||
);
|
||||
// Materiau PBR : c'est ce qui fait passer le rendu par `fs_pbr` et non `fs_main`.
|
||||
scene.add_material_pbr("sol", "standard", 0.1, 0.6).unwrap();
|
||||
scene
|
||||
.set_material_color("sol", [1.0, 1.0, 1.0, 1.0])
|
||||
.unwrap();
|
||||
scene
|
||||
.create_mesh(
|
||||
"sol",
|
||||
crate::mesh::primitives::plane(24.0, 24.0, 1, 1),
|
||||
Some("sol"),
|
||||
)
|
||||
.unwrap();
|
||||
scene
|
||||
.add_entity_with_transform("sol", "sol", crate::core::Transform::identity())
|
||||
.unwrap();
|
||||
scene.clear_lights();
|
||||
scene.set_ambient([0.0, 0.0, 0.0]);
|
||||
scene
|
||||
.add_spot_light(
|
||||
glam::Vec3::new(0.0, 4.0, 0.0),
|
||||
glam::Vec3::new(0.0, -1.0, 0.0),
|
||||
[1.0, 1.0, 1.0],
|
||||
1.0,
|
||||
14.0,
|
||||
outer,
|
||||
)
|
||||
.unwrap();
|
||||
let cam = scene.camera_mut();
|
||||
*cam = crate::camera::Camera::perspective(
|
||||
glam::Vec3::new(0.0, 7.0, 10.0),
|
||||
glam::Vec3::new(0.0, 0.0, 0.0),
|
||||
glam::Vec3::Y,
|
||||
)
|
||||
.with_perspective(std::f32::consts::FRAC_PI_4, 0.1, 100.0);
|
||||
scene
|
||||
};
|
||||
|
||||
let rends = |mut scene: crate::scene::Scene| -> Vec<u8> {
|
||||
let renderer = crate::core::Renderer::new(
|
||||
&context,
|
||||
wgpu::TextureFormat::Rgba8Unorm,
|
||||
W,
|
||||
H,
|
||||
&crate::core::shadow::ShadowConfig::default(),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
false,
|
||||
false,
|
||||
);
|
||||
let tex = context.device.create_texture(&wgpu::TextureDescriptor {
|
||||
label: Some("spot-pbr"),
|
||||
size: wgpu::Extent3d {
|
||||
width: W,
|
||||
height: H,
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: wgpu::TextureDimension::D2,
|
||||
format: wgpu::TextureFormat::Rgba8Unorm,
|
||||
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
|
||||
view_formats: &[],
|
||||
});
|
||||
let view = tex.create_view(&Default::default());
|
||||
for _ in 0..3 {
|
||||
renderer.render_scene(&view, &mut scene, W as f32 / H as f32, 1.0, 0.016);
|
||||
}
|
||||
crate::core::testutil::readback_texture_rgba8(&context.device, &context.queue, &tex, W, H)
|
||||
};
|
||||
|
||||
let luma = |px: &[u8], x: usize, y: usize| -> u32 {
|
||||
let i = (y * W as usize + x) * 4;
|
||||
(px[i] as u32 + px[i + 1] as u32 + px[i + 2] as u32) / 3
|
||||
};
|
||||
|
||||
let large = rends(construis(0.45));
|
||||
let etroit = rends(construis(0.06));
|
||||
let y = (H / 2) as usize;
|
||||
let centre = luma(&large, (W / 2) as usize, y);
|
||||
let centre_etroit = luma(&etroit, (W / 2) as usize, y);
|
||||
|
||||
// Le PBR a un plancher (gotcha 6.14 : `spec_ibl` rend une luminance non nulle meme ambiante a
|
||||
// zero) et ce plancher N'EST PAS partage entre une jambe ponctuelle et une jambe cone : mesure,
|
||||
// le sol non eclaire valait 13 sous cone et 32 sous ponctuelle, la ponctuelle lavant toute la
|
||||
// rangee. Un rapport centre/bord est donc dilue (4,62 contre 2,07, soit 2,23 de separation, trop
|
||||
// peu pour un seuil). Le discriminant propre a `fs_pbr` est la **sensibilite a l'angle** : deux
|
||||
// rendus PBR qui ne different que par l'angle exterieur du cone. Si le terme cone n'est pas
|
||||
// applique, les deux images sont identiques — et l'identite ne depend d'aucun seuil.
|
||||
let plancher = {
|
||||
// Le niveau du sol non eclaire, mesure sur la jambe au cone etroit (le sol y est presque
|
||||
// entier dans le noir) et partage entre les deux jambes : c'est une propriete du materiau et de
|
||||
// l'IBL, pas de l'angle. La rangee utilisee est celle du pied du cone (milieu de l'image) : la
|
||||
// rangee du bas ne croise pas la tache — mesure, elle rendait 0 dans les deux jambes.
|
||||
let mut v: Vec<u32> = (0..W as usize).map(|x| luma(&etroit, x, y)).collect();
|
||||
v.sort_unstable();
|
||||
v[v.len() / 2]
|
||||
};
|
||||
let aire_ligne = |px: &[u8]| -> usize {
|
||||
(0..W as usize)
|
||||
.filter(|x| luma(px, *x, y) > plancher + 10)
|
||||
.count()
|
||||
};
|
||||
let aire_large = aire_ligne(&large);
|
||||
let aire_etroite = aire_ligne(&etroit);
|
||||
let diff: usize = (large
|
||||
.chunks_exact(4)
|
||||
.zip(etroit.chunks_exact(4))
|
||||
.filter(|(a, b)| a != b)
|
||||
.count());
|
||||
eprintln!(
|
||||
"15.5 spot PBR : plancher={plancher} | large aire_ligne={aire_large} centre={centre} | etroit aire_ligne={aire_etroite} centre={centre_etroit} | texels differents={diff}"
|
||||
);
|
||||
|
||||
// Controle positif : la scene rend (sinon tout ce qui suit est satisfait par le vide).
|
||||
assert!(
|
||||
centre > 40,
|
||||
"le pied du cone en PBR ne rend que {centre} texel-luma : la scene n'est pas mesurable"
|
||||
);
|
||||
assert!(
|
||||
aire_large > 20,
|
||||
"le cone large n'eclaire que {aire_large} texels sur la rangee du sol : faisceau absent de `fs_pbr`, ou scene mal cadree"
|
||||
);
|
||||
// P1 : l'angle du cone est applique en PBR. Identite = le terme cone n'est pas dans `fs_pbr`.
|
||||
assert!(
|
||||
diff > 1_000,
|
||||
"changer l'angle exterieur du cone (0,45 -> 0,06) ne change que {diff} texels en PBR : le terme cone n'est pas applique par `fs_pbr` — le faisceau y est un pied de lumiere (famille 6.7, brancher dans TOUS les points d'entree)"
|
||||
);
|
||||
// P2 : l'angle agit, mais **peu**. Mesure 2026-11-03 : 80 texels eclaires sur la rangee du pied du
|
||||
// cone pour half_angle 0,45 contre **50** pour 0,06. Un facteur 2 echouait (100 > 80) : ce qui tient
|
||||
// est l'ordre, double d'une borne de non-vacuite pour que l'ordre ne soit pas satisfait par un 0 muet.
|
||||
assert!(
|
||||
aire_etroite < aire_large,
|
||||
"aire eclairee sur la rangee du pied du cone : cone large {aire_large} texels contre cone etroit {aire_etroite} (mesure 80 / 50) — l'angle ne retrecit plus la tache en PBR"
|
||||
);
|
||||
assert!(
|
||||
aire_etroite >= 5,
|
||||
"le cone etroit n'eclaire que {aire_etroite} texels : la jambe comparative est vide, l'ordre des aires ne prouve rien"
|
||||
);
|
||||
// P3 — **caracterisation** (elle rougira le jour ou une veritable penombre d'angle sera implantee,
|
||||
// et ce sera une bonne nouvelle) : le cône exterieur du moteur vaut `cos(half_angle) - 0.1` en
|
||||
// espace COSINUS (standard_shader.wgsl, bloc des spots). Pour half_angle = 0,06 rad (3,4 degres),
|
||||
// cos = 0,998, moins 0,1 = 0,898, soit un cône exterieur de **26 degres** : diviser l'angle interne
|
||||
// par 7 ne divise que de 80 a 50 la tache eclairee. Un appelant qui regle `half_angle` pour viser
|
||||
// fin obtient donc une tache beaucoup plus large que demande — sans erreur ni log.
|
||||
assert!(
|
||||
aire_etroite * 3 > aire_large,
|
||||
"le cone etroit ({aire_etroite} texels) est desormais plus de 3 fois plus etroit que le cone large ({aire_large}) : la penombre n'est plus un decalage fixe de 0,1 en cosinus — relire ce verrou et la formule `cos_outer = cos_inner - 0.1`, et retrecir le caractere de cette caracterisation"
|
||||
);
|
||||
}
|
||||
@@ -828,7 +828,13 @@ pub fn bands_below(img: &[u8], w: u32, h: u32, y0: u32) -> GroundBands {
|
||||
/// Boîte englobante des pixels du sol dont la luminance est sous `seuil` (ombres comprises), avec
|
||||
/// le nombre de pixels concernés. Une boîte englobante sans comptage ne dit pas si la forme est
|
||||
/// pleine ou creuse : les deux grandeurs sont renvoyées.
|
||||
pub fn shadow_bbox_below(img: &[u8], w: u32, h: u32, y0: u32, seuil: u32) -> (Option<(u32, u32, u32, u32)>, u32) {
|
||||
pub fn shadow_bbox_below(
|
||||
img: &[u8],
|
||||
w: u32,
|
||||
h: u32,
|
||||
y0: u32,
|
||||
seuil: u32,
|
||||
) -> (Option<(u32, u32, u32, u32)>, u32) {
|
||||
let (mut x0, mut x1, mut y1min, mut y1max, mut n) = (u32::MAX, 0u32, u32::MAX, 0u32, 0u32);
|
||||
for y in y0..h {
|
||||
for x in 0..w {
|
||||
|
||||
@@ -71,14 +71,19 @@ fn faces(geo: &Geometry) -> Vec<Face> {
|
||||
corners: Vec::new(),
|
||||
};
|
||||
for &i in tri {
|
||||
f.corners
|
||||
.push((Vec3::from_array(positions[i as usize]), Vec2::from_array(uvs[i as usize])));
|
||||
f.corners.push((
|
||||
Vec3::from_array(positions[i as usize]),
|
||||
Vec2::from_array(uvs[i as usize]),
|
||||
));
|
||||
}
|
||||
out.push(f);
|
||||
continue;
|
||||
};
|
||||
for &i in tri {
|
||||
let c = (Vec3::from_array(positions[i as usize]), Vec2::from_array(uvs[i as usize]));
|
||||
let c = (
|
||||
Vec3::from_array(positions[i as usize]),
|
||||
Vec2::from_array(uvs[i as usize]),
|
||||
);
|
||||
if !f.corners.iter().any(|x| (x.0 - c.0).length() < 1e-4) {
|
||||
f.corners.push(c);
|
||||
}
|
||||
@@ -142,8 +147,9 @@ fn rendre_face(
|
||||
"{}/examples/assets/textures/uv_texture.jpg",
|
||||
env!("CARGO_MANIFEST_DIR")
|
||||
);
|
||||
let texture = crate::resources::Texture::from_file(&context.device, &context.queue, "atlas", &path)
|
||||
.expect("atlas de l'exemple");
|
||||
let texture =
|
||||
crate::resources::Texture::from_file(&context.device, &context.queue, "atlas", &path)
|
||||
.expect("atlas de l'exemple");
|
||||
scene.add_texture("uv_texture", texture).expect("texture");
|
||||
scene
|
||||
.add_material_texture("mat", "standard", "uv_texture")
|
||||
@@ -193,16 +199,13 @@ fn rendre_face(
|
||||
/// Position d'un point monde dans l'image (pixels, origine en haut à gauche).
|
||||
///
|
||||
/// `transform_point3` ne divise PAS par `w` (gotcha 14-F6) : la division est faite ici.
|
||||
fn vers_image(
|
||||
scene_cam: &crate::camera::Camera,
|
||||
aspect: f32,
|
||||
p: Vec3,
|
||||
w: u32,
|
||||
h: u32,
|
||||
) -> Vec2 {
|
||||
fn vers_image(scene_cam: &crate::camera::Camera, aspect: f32, p: Vec3, w: u32, h: u32) -> Vec2 {
|
||||
let clip = scene_cam.projection_matrix(aspect) * scene_cam.view_matrix() * p.extend(1.0);
|
||||
let ndc = Vec2::new(clip.x / clip.w, clip.y / clip.w);
|
||||
Vec2::new((ndc.x * 0.5 + 0.5) * w as f32, (0.5 - ndc.y * 0.5) * h as f32)
|
||||
Vec2::new(
|
||||
(ndc.x * 0.5 + 0.5) * w as f32,
|
||||
(0.5 - ndc.y * 0.5) * h as f32,
|
||||
)
|
||||
}
|
||||
|
||||
/// Moyenne RGBA d'un carré de `RAYON` texels centré en `(x, y)`.
|
||||
@@ -305,12 +308,7 @@ fn carte_uv_ecran(f: &Face, cam: &crate::camera::Camera) -> [[i32; 2]; 2] {
|
||||
};
|
||||
let u = coin(1.0, 0.0) - coin(0.0, 0.0);
|
||||
let v = coin(0.0, 1.0) - coin(0.0, 0.0);
|
||||
let ligne = |d: Vec3| {
|
||||
[
|
||||
d.dot(x_hat).round() as i32,
|
||||
d.dot(y_hat).round() as i32,
|
||||
]
|
||||
};
|
||||
let ligne = |d: Vec3| [d.dot(x_hat).round() as i32, d.dot(y_hat).round() as i32];
|
||||
[ligne(u), ligne(v)]
|
||||
}
|
||||
|
||||
@@ -354,7 +352,9 @@ struct Lumieres {
|
||||
}
|
||||
|
||||
fn lumieres_de_l_exemple() -> Lumieres {
|
||||
let src = crate::core::testutil::strip_line_comments(&crate::core::testutil::example_source(CUBE_EXEMPLE));
|
||||
let src = crate::core::testutil::strip_line_comments(&crate::core::testutil::example_source(
|
||||
CUBE_EXEMPLE,
|
||||
));
|
||||
let plat = src.split_whitespace().collect::<Vec<&str>>().join(" ");
|
||||
|
||||
let args_de = |marqueur: &str| -> Vec<String> {
|
||||
@@ -417,13 +417,15 @@ fn lumieres_de_l_exemple() -> Lumieres {
|
||||
.and_then(|t| t.strip_suffix(')'))
|
||||
.or_else(|| s.strip_prefix('[').and_then(|t| t.strip_suffix(']')))
|
||||
.unwrap_or_else(|| panic!("attendu Vec3::new(..) ou [..], lu {s}"));
|
||||
let v: Vec<f32> = interieur.split(',').map(|x| x.trim().parse().expect("flottant")).collect();
|
||||
let v: Vec<f32> = interieur
|
||||
.split(',')
|
||||
.map(|x| x.trim().parse().expect("flottant"))
|
||||
.collect();
|
||||
assert_eq!(v.len(), 3, "{s} : {} composants", v.len());
|
||||
[v[0], v[1], v[2]]
|
||||
};
|
||||
let champ_f32 = |s: &str| -> f32 {
|
||||
s.trim().trim_end_matches("f32").parse().expect("flottant")
|
||||
};
|
||||
let champ_f32 =
|
||||
|s: &str| -> f32 { s.trim().trim_end_matches("f32").parse().expect("flottant") };
|
||||
|
||||
let p = args_de("add_point_light(");
|
||||
assert_eq!(p.len(), 4, "add_point_light : {} arguments", p.len());
|
||||
@@ -481,10 +483,7 @@ fn banc_atlas_affiche_coins_classement_et_luminances() {
|
||||
moyennes.push(patch(&img, W, H, cpx.x, cpx.y, 12));
|
||||
images.push(img);
|
||||
}
|
||||
let ref_i = fs
|
||||
.iter()
|
||||
.position(|f| f.normal.z > 0.5)
|
||||
.expect("face +Z");
|
||||
let ref_i = fs.iter().position(|f| f.normal.z > 0.5).expect("face +Z");
|
||||
let cartes: Vec<[[i32; 2]; 2]> = fs
|
||||
.iter()
|
||||
.map(|f| {
|
||||
@@ -524,7 +523,10 @@ fn banc_atlas_affiche_coins_classement_et_luminances() {
|
||||
}
|
||||
for i in 0..6 {
|
||||
for j in i + 1..6 {
|
||||
eprintln!("[unlit] faces {i}↔{j} écart de moyenne {:.1}", ecart(&moyennes[i], &moyennes[j]));
|
||||
eprintln!(
|
||||
"[unlit] faces {i}↔{j} écart de moyenne {:.1}",
|
||||
ecart(&moyennes[i], &moyennes[j])
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -543,7 +545,10 @@ fn banc_atlas_affiche_coins_classement_et_luminances() {
|
||||
let cam = crate::camera::Camera::perspective(eye, Vec3::ZERO, Vec3::Y);
|
||||
let vue = Vec3::new(-eye.x, -eye.y, -eye.z).normalize();
|
||||
let visibles: Vec<&Face> = fs.iter().filter(|f| f.normal.dot(vue) > 0.35).collect();
|
||||
eprintln!("[lumieres] {:?} ponctuelle {:?} projecteur", l.ponctuelle_pos, l.projecteur_pos);
|
||||
eprintln!(
|
||||
"[lumieres] {:?} ponctuelle {:?} projecteur",
|
||||
l.ponctuelle_pos, l.projecteur_pos
|
||||
);
|
||||
eprintln!("[vues] {} faces", visibles.len());
|
||||
let centre_de = |f: &Face| f.corners.iter().map(|(p, _)| *p).sum::<Vec3>() / 4.0;
|
||||
let mut a = Vec::new();
|
||||
@@ -727,8 +732,14 @@ fn trois_faces_eclairees_de_facons_visiblement_differentes() {
|
||||
// octet (mesuré 0,00). Sans cette jambe, les écarts des jambes suivantes pourraient être du bruit
|
||||
// de rastérisation.
|
||||
for i in 0..3 {
|
||||
let d = ecart(&moyenne_de(&unlit, visibles[i]), &moyenne_de(&unlit2, visibles[i]));
|
||||
assert_eq!(d, 0.0, "le rendu unlit de la face {i} n'est pas reproductible (écart {d:.2})");
|
||||
let d = ecart(
|
||||
&moyenne_de(&unlit, visibles[i]),
|
||||
&moyenne_de(&unlit2, visibles[i]),
|
||||
);
|
||||
assert_eq!(
|
||||
d, 0.0,
|
||||
"le rendu unlit de la face {i} n'est pas reproductible (écart {d:.2})"
|
||||
);
|
||||
}
|
||||
|
||||
// 2. Ce que le bandeau affirme : trois luminances deux à deux distinctes. Bornes mesurées : le
|
||||
@@ -737,7 +748,10 @@ fn trois_faces_eclairees_de_facons_visiblement_differentes() {
|
||||
// écarts qui s'effondreraient — c'est la jambe falsifiable, vue ROUGE par M3 de la campagne.
|
||||
for i in 0..3 {
|
||||
for j in i + 1..3 {
|
||||
let d = ecart(&moyenne_de(&lit, visibles[i]), &moyenne_de(&lit, visibles[j]));
|
||||
let d = ecart(
|
||||
&moyenne_de(&lit, visibles[i]),
|
||||
&moyenne_de(&lit, visibles[j]),
|
||||
);
|
||||
assert!(
|
||||
d > 100.0,
|
||||
"les faces éclairées {i} et {j} ne different que de {d:.1} (mesuré 154,0 au pire) — soit l'ombrage est plat (normales fausses), soit la lumière n'atteint pas la scène"
|
||||
@@ -750,11 +764,13 @@ fn trois_faces_eclairees_de_facons_visiblement_differentes() {
|
||||
// ignorées par le shader (mutation M3 de `campaigns/mutate-cube.py`), l'écart tombe à **0,00**.
|
||||
// Le seuil est à 12, soit deux fois sous la pire mesure propre et deux fois au-dessus de zéro.
|
||||
for i in 0..3 {
|
||||
let d = ecart(&moyenne_de(&lit, visibles[i]), &moyenne_de(&deplace, visibles[i]));
|
||||
let d = ecart(
|
||||
&moyenne_de(&lit, visibles[i]),
|
||||
&moyenne_de(&deplace, visibles[i]),
|
||||
);
|
||||
assert!(
|
||||
d > 12.0,
|
||||
"retirer les lumières de l'exemple ne change pas la face {i} (écart {d:.1}, propre mesuré 24,1 au pire, mutation 0,00) — l'écart inter-faces de la jambe 2 ne serait pas dû à la lumière"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1583,7 +1583,6 @@ fn bandeau_from_calls(src: &str, marqueur: &str) -> Vec<(String, String)> {
|
||||
let champs: Vec<&str> = args.split(',').map(|s| s.trim()).collect();
|
||||
if champs.len() >= 4 {
|
||||
out.push((String::new(), format!("{}, {}", champs[2], champs[3])));
|
||||
|
||||
}
|
||||
}
|
||||
out
|
||||
@@ -1591,9 +1590,9 @@ fn bandeau_from_calls(src: &str, marqueur: &str) -> Vec<(String, String)> {
|
||||
|
||||
/// Verrou 15.5 — le bandeau d'`env_map` promet « six spheres differing ONLY in metallic and \
|
||||
/// roughness ». C'est une affirmation sur la **démonstration**, et elle est vérifiable à la source :
|
||||
/// six paires distinctes, une **seule** géométrie partagée, et une seule fabrique de matériau dans la
|
||||
/// boucle. Sans ce verrou, ajouter une septième sphère à un autre mesh, ou faire varier la couleur,
|
||||
/// rendrait la phrase fausse sans qu'aucun test ne le dise (famille Q1 : un verrou porté sur une scène
|
||||
/// six paires distinctes, une **seule** géométrie partagée, et une seule fabrique de matériau dans la
|
||||
/// boucle. Sans ce verrou, ajouter une septième sphère à un autre mesh, ou faire varier la couleur,
|
||||
/// rendrait la phrase fausse sans qu'aucun test ne le dise (famille Q1 : un verrou porté sur une scène
|
||||
/// de test ne dit rien de l'exemple livré).
|
||||
#[test]
|
||||
fn les_six_spherees_denv_map_ne_different_que_par_metallic_et_roughness() {
|
||||
@@ -1630,26 +1629,48 @@ fn les_six_spherees_denv_map_ne_different_que_par_metallic_et_roughness() {
|
||||
))
|
||||
})
|
||||
.collect();
|
||||
assert_eq!(paires.len(), 6, "la table SPHERES a {} entrees, le bandeau en promet six", paires.len());
|
||||
assert_eq!(
|
||||
paires.len(),
|
||||
6,
|
||||
"la table SPHERES a {} entrees, le bandeau en promet six",
|
||||
paires.len()
|
||||
);
|
||||
let mut distinctes = paires.clone();
|
||||
distinctes.sort_by(|a, b| a.partial_cmp(b).unwrap());
|
||||
distinctes.dedup();
|
||||
assert_eq!(distinctes.len(), 6, "deux spheres d'env_map partagent la meme paire metallic/roughness : le contraste promis n'existe pas");
|
||||
assert_eq!(
|
||||
distinctes.len(),
|
||||
6,
|
||||
"deux spheres d'env_map partagent la meme paire metallic/roughness : le contraste promis n'existe pas"
|
||||
);
|
||||
|
||||
// 2. « ONLY » : une seule geometrie spherique, construite une fois et partagee par les six corps ;
|
||||
// et, dans le corps de la boucle, la seule chose qui varie est le couple (metallic, roughness).
|
||||
// Compter les `create_mesh` ou les `add_material_pbr` de tout le fichier ne dit rien : l'exemple
|
||||
// cree aussi un sol — un verrou qui rougit sur un fait legitime apprend a le contourner (14-F6).
|
||||
let spheres = src.matches("uv_sphere(").count();
|
||||
assert_eq!(spheres, 1, "env_map construit {spheres} geometries spheriques : les six corps ne partagent plus la meme forme, la phrase « ONLY in metallic and roughness » est fausse");
|
||||
assert_eq!(
|
||||
spheres, 1,
|
||||
"env_map construit {spheres} geometries spheriques : les six corps ne partagent plus la meme forme, la phrase « ONLY in metallic and roughness » est fausse"
|
||||
);
|
||||
|
||||
let amorce = src
|
||||
.find("for (i, (metallic, roughness))")
|
||||
.expect("la boucle des six spheres est identifiable");
|
||||
let corps = &src[amorce..amorce + src[amorce..].find("\n }").expect("le corps de la boucle se termine")];
|
||||
assert!(corps.contains("*metallic") && corps.contains("*roughness"), "la boucle des spheres ne transmet plus le couple metallic/roughness comme seuls parametres variables");
|
||||
let corps = &src[amorce
|
||||
..amorce
|
||||
+ src[amorce..]
|
||||
.find("\n }")
|
||||
.expect("le corps de la boucle se termine")];
|
||||
assert!(
|
||||
corps.contains("*metallic") && corps.contains("*roughness"),
|
||||
"la boucle des spheres ne transmet plus le couple metallic/roughness comme seuls parametres variables"
|
||||
);
|
||||
for interdite in ["with_color", "set_color", "with_emissive", ".scale"] {
|
||||
assert!(!corps.contains(interdite), "la boucle des spheres applique {interdite} : les six corps different par autre chose que metallic/roughness — la phrase du bandeau est fausse (limite écrite : le mot est banni dans le corps de la boucle, commentaires compris)");
|
||||
assert!(
|
||||
!corps.contains(interdite),
|
||||
"la boucle des spheres applique {interdite} : les six corps different par autre chose que metallic/roughness — la phrase du bandeau est fausse (limite écrite : le mot est banni dans le corps de la boucle, commentaires compris)"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1667,12 +1688,20 @@ fn le_bandeau_de_repetition_break_correspond_aux_reglages_de_l_exemple() {
|
||||
.match_indices("set_material_repetition_break(")
|
||||
.map(|(i, _)| i)
|
||||
.collect();
|
||||
assert_eq!(appels.len(), 4, "l'exemple applique la rupture de repetition a {} sites, le bandeau en attend quatre (montage x2 + touche x2)", appels.len());
|
||||
assert_eq!(
|
||||
appels.len(),
|
||||
4,
|
||||
"l'exemple applique la rupture de repetition a {} sites, le bandeau en attend quatre (montage x2 + touche x2)",
|
||||
appels.len()
|
||||
);
|
||||
|
||||
// Le groupe de gauche est le TEMOIN : aucun site ne le cible.
|
||||
for site in &appels {
|
||||
let apres = &src[*site..(*site + 200).min(src.len())];
|
||||
assert!(!apres.contains("left_"), "un site de reglages cible un materiau « left_ » : le groupe de gauche n'est plus le temoin, la phrase « no technique is set on it » est fausse");
|
||||
assert!(
|
||||
!apres.contains("left_"),
|
||||
"un site de reglages cible un materiau « left_ » : le groupe de gauche n'est plus le temoin, la phrase « no technique is set on it » est fausse"
|
||||
);
|
||||
}
|
||||
|
||||
// Touche 1 → milieu seulement ; touche 2 → droite seulement.
|
||||
@@ -1680,17 +1709,32 @@ fn le_bandeau_de_repetition_break_correspond_aux_reglages_de_l_exemple() {
|
||||
let d2 = src.find("Digit2").expect("la touche 2 est geree");
|
||||
let zone1 = &src[d1..d2];
|
||||
let zone2 = &src[d2..];
|
||||
assert!(zone1.contains("middle_") && !zone1.contains("right_"), "la touche 1 ne regle plus le seul groupe du milieu : la separation promise des deux groupes ne tient plus");
|
||||
assert!(zone2.contains("right_") && !zone2.contains("middle_"), "la touche 2 ne regle plus le seul groupe de droite");
|
||||
assert!(
|
||||
zone1.contains("middle_") && !zone1.contains("right_"),
|
||||
"la touche 1 ne regle plus le seul groupe du milieu : la separation promise des deux groupes ne tient plus"
|
||||
);
|
||||
assert!(
|
||||
zone2.contains("right_") && !zone2.contains("middle_"),
|
||||
"la touche 2 ne regle plus le seul groupe de droite"
|
||||
);
|
||||
|
||||
// Le groupe de droite porte le bruit SEUL (drapeau 1), jamais les deux techniques (3).
|
||||
assert!(!src.contains("flags_right: 3") && !src.contains("flags_right = 3"), "le groupe de droite recoit les deux techniques : la phrase « the noise-blend ALONE, no UV warping » est fausse");
|
||||
assert!(
|
||||
!src.contains("flags_right: 3") && !src.contains("flags_right = 3"),
|
||||
"le groupe de droite recoit les deux techniques : la phrase « the noise-blend ALONE, no UV warping » est fausse"
|
||||
);
|
||||
|
||||
// Retendre la touche remet le groupe dans la repetition brute : les deux drapeaux sont bien
|
||||
// **bascules**, pas seulement arms (c'est ce que promet « Press 1 again and the middle returns
|
||||
// to raw repetition »).
|
||||
assert!(src.contains("self.flags_middle = if self.flags_middle"), "la touche 1 n'est plus une bascule : le groupe du milieu ne peut pas revenir a la repetition brute");
|
||||
assert!(src.contains("self.flags_right = if self.flags_right"), "la touche 2 n'est plus une bascule : le groupe de droite ne peut pas revenir a la repetition brute");
|
||||
assert!(
|
||||
src.contains("self.flags_middle = if self.flags_middle"),
|
||||
"la touche 1 n'est plus une bascule : le groupe du milieu ne peut pas revenir a la repetition brute"
|
||||
);
|
||||
assert!(
|
||||
src.contains("self.flags_right = if self.flags_right"),
|
||||
"la touche 2 n'est plus une bascule : le groupe de droite ne peut pas revenir a la repetition brute"
|
||||
);
|
||||
}
|
||||
|
||||
/// Verrou 15.5 — le gotcha 6.26 (« ne jamais garder un mode avec `>=` ») n'était porté que par la prose
|
||||
@@ -1711,7 +1755,10 @@ fn le_mode_de_repetition_est_compare_exactement_pas_par_une_borne() {
|
||||
.expect("le mode de repetition est arrondi avant comparaison (gotcha 6.26 : un u8 transporte en f32)");
|
||||
let zone = &shader[amorce..amorce + 600.min(shader.len() - amorce)];
|
||||
for interdite in ["rmode >=", "rmode <=", "rmode >", "rmode <"] {
|
||||
assert!(!zone.contains(interdite), "le mode de repetition est compare par une borne ({interdite}) : le mode 1 satisfait « >= 1 » et devient identique au mode 3 — gotcha 6.26 rejoue");
|
||||
assert!(
|
||||
!zone.contains(interdite),
|
||||
"le mode de repetition est compare par une borne ({interdite}) : le mode 1 satisfait « >= 1 » et devient identique au mode 3 — gotcha 6.26 rejoue"
|
||||
);
|
||||
}
|
||||
assert!(
|
||||
zone.contains("rmode == 1u") && zone.contains("rmode == 3u"),
|
||||
|
||||
@@ -60,7 +60,7 @@ fn repo_root() -> std::path::PathBuf {
|
||||
///
|
||||
/// Il ne peut que **descendre**, en écrivant des lignes de manifeste. Le laisser à 999 serait un
|
||||
/// verrou décoratif.
|
||||
const UNCOVERED_EXAMPLES_RATCHET: usize = 1; // mesure 2026-11-03 apres fermeture de deformable // mesure 2026-11-03 apres fermeture de collision // mesure 2026-11-03 apres fermeture de manual // mesure 2026-11-03 apres fermeture de picking // mesure 2026-11-03 apres fermeture de spot_test // mesure 2026-11-03 apres fermeture de instances_sim // mesure 2026-11-03 apres fermeture de instances // mesure 2026-11-03 apres fermeture de repetition_break // mesure 2026-11-03 apres fermeture de transparency // mesure 2026-11-03 apres fermeture de env_map // mesure 2026-11-03 apres fermeture de ssao // mesure 2026-11-03 apres fermeture de dof // mesuré 2026-11-03 : chaîne 33 → 30 → 16 → 15 (`bloom`) → 14 (`emissive`) → 13 (`hdr`, trois verrous neufs de `core::hdr_tests`)
|
||||
const UNCOVERED_EXAMPLES_RATCHET: usize = 0; // mesure 2026-11-03 apres fermeture de demo : plus aucun exemple sans ligne de manifeste // mesure 2026-11-03 apres fermeture de deformable // mesure 2026-11-03 apres fermeture de collision // mesure 2026-11-03 apres fermeture de manual // mesure 2026-11-03 apres fermeture de picking // mesure 2026-11-03 apres fermeture de spot_test // mesure 2026-11-03 apres fermeture de instances_sim // mesure 2026-11-03 apres fermeture de instances // mesure 2026-11-03 apres fermeture de repetition_break // mesure 2026-11-03 apres fermeture de transparency // mesure 2026-11-03 apres fermeture de env_map // mesure 2026-11-03 apres fermeture de ssao // mesure 2026-11-03 apres fermeture de dof // mesuré 2026-11-03 : chaîne 33 → 30 → 16 → 15 (`bloom`) → 14 (`emissive`) → 13 (`hdr`, trois verrous neufs de `core::hdr_tests`)
|
||||
// Chaîne des mesures : 37 → **36** (`fog` est passé du côté couvert — trois lignes de manifeste, dont
|
||||
// la phrase falsifiable « the fog VANISHES », verrouillée par
|
||||
// `the_fog_example_is_measurable_at_its_own_framing`) → **35** (`procedural_textures`, 5 lignes de
|
||||
|
||||
@@ -958,3 +958,56 @@ fn regex_rows_deep(text: &str) -> Vec<u64> {
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Verrou 15.5 — le bandeau de `effects/demo` enonce deux inventaires : six primitives procedurales,
|
||||
/// et un melange de **sources** de texture (deux assets fichier, deux motifs proceduraux). L'exemple
|
||||
/// est la porte d'entree de l'API (« Read this file first ») ; rien ne confrontait ces enonces au
|
||||
/// fichier. Un exemple de demonstration perd une primitive en silence des qu'on le « nettoie ».
|
||||
#[test]
|
||||
fn le_demo_declare_chaque_primitive_et_chaque_source_de_texture_qu_il_nomme() {
|
||||
let chemin = crate_root().join("examples/effects/demo.rs");
|
||||
let src = strip_comments(&std::fs::read_to_string(&chemin).expect("examples/effects/demo.rs"));
|
||||
|
||||
let primitives = [
|
||||
("cube", "cube("),
|
||||
("sphere", "uv_sphere("),
|
||||
("icosphere", "icosphere("),
|
||||
("cylinder", "cylinder("),
|
||||
("cone", "cone("),
|
||||
("torus", "torus("),
|
||||
];
|
||||
for (nom, appelee) in primitives {
|
||||
assert!(
|
||||
src.contains(appelee),
|
||||
"le bandeau de `demo` nomme la primitive `{nom}`, mais `demo.rs` n'appelle plus `{appelee}` : la vitrine annonce une forme qu'elle ne montre plus"
|
||||
);
|
||||
}
|
||||
// Non-vacuite : six primitives, pas une seule rencontre fortuite.
|
||||
let trouvees = primitives.iter().filter(|(_, a)| src.contains(a)).count();
|
||||
assert_eq!(
|
||||
trouvees, 6,
|
||||
"seulement {trouvees} des six primitives nommees sont construites"
|
||||
);
|
||||
|
||||
// Les sources de texture : deux fichiers (asset + atlas) et deux motifs proceduraux generes ici.
|
||||
let fichiers = occurrences(&src, "from_file(");
|
||||
let procedurales = occurrences(&src, "from_rgba8(");
|
||||
assert_eq!(
|
||||
fichiers, 2,
|
||||
"`demo` est cense charger DEUX assets fichier (`ground.jpeg`, `uv_texture.jpg`) ; trouve {fichiers} appel(s) a `from_file(`"
|
||||
);
|
||||
assert_eq!(
|
||||
procedurales, 2,
|
||||
"`demo` est cense generer DEUX motifs proceduraux ; trouve {procedurales} appel(s) a `from_rgba8(`"
|
||||
);
|
||||
for asset in ["ground.jpeg", "uv_texture.jpg"] {
|
||||
assert!(
|
||||
src.contains(asset),
|
||||
"`demo` ne nomme plus l'asset `{asset}` que son bandeau presente comme une source de texture"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn occurrences(src: &str, aiguille: &str) -> usize {
|
||||
src.matches(aiguille).count()
|
||||
}
|
||||
@@ -56,7 +56,12 @@ fn groupes(exemple: &str, début: &str) -> Vec<String> {
|
||||
']' => {
|
||||
if let Some(g) = courant.take() {
|
||||
if g.split(',').all(|x| x.trim().parse::<f32>().is_ok()) {
|
||||
groupes.push(g.split(',').map(|x| x.trim().to_string()).collect::<Vec<_>>().join(","));
|
||||
groupes.push(
|
||||
g.split(',')
|
||||
.map(|x| x.trim().to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join(","),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -87,20 +92,37 @@ fn les_exemples_simple_et_manuel_construisent_le_meme_quadrilatere() {
|
||||
|
||||
let pos_simple = groupes(&simple, "Geometry::new(vec![");
|
||||
let pos_manuel = groupes(&manuel, "Geometry::new(vec![");
|
||||
assert!(pos_simple.len() >= 4, "simple : {}/{}, extraire moins de 4 sommets veut dire que le parseur ne trouve plus le segment — le verrou devient décor", pos_simple.len(), 4);
|
||||
assert_eq!(pos_simple, pos_manuel, "les deux exemples ne posent plus les mêmes sommets : la promesse « the same flat unlit quad as the `simple` example » est fausse");
|
||||
assert!(
|
||||
pos_simple.len() >= 4,
|
||||
"simple : {}/{}, extraire moins de 4 sommets veut dire que le parseur ne trouve plus le segment — le verrou devient décor",
|
||||
pos_simple.len(),
|
||||
4
|
||||
);
|
||||
assert_eq!(
|
||||
pos_simple, pos_manuel,
|
||||
"les deux exemples ne posent plus les mêmes sommets : la promesse « the same flat unlit quad as the `simple` example » est fausse"
|
||||
);
|
||||
|
||||
let col_simple = groupes(&simple, ".with_colors(vec![");
|
||||
let col_manuel = groupes(&manuel, ".with_colors(vec![");
|
||||
assert!(!col_simple.is_empty() && !col_manuel.is_empty(), "l'un des deux exemples n'a plus de bloc .with_colors(vec![ : le verrou ne compare plus les couleurs");
|
||||
assert_eq!(col_simple, col_manuel, "les deux exemples ne colorent plus le quad de la même façon");
|
||||
assert!(
|
||||
!col_simple.is_empty() && !col_manuel.is_empty(),
|
||||
"l'un des deux exemples n'a plus de bloc .with_colors(vec![ : le verrou ne compare plus les couleurs"
|
||||
);
|
||||
assert_eq!(
|
||||
col_simple, col_manuel,
|
||||
"les deux exemples ne colorent plus le quad de la même façon"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn les_deux_exemples_arment_le_mode_unlit() {
|
||||
for rel in ["examples/meshes/simple.rs", "examples/meshes/manual.rs"] {
|
||||
let src = strip_comments(&lire(rel));
|
||||
assert!(src.contains("set_unlit(true)"), "{rel} n'arme plus set_unlit(true) : la promesse « unlit » du bandeau de `manual` n'est plus garantie par les deux exemples");
|
||||
assert!(
|
||||
src.contains("set_unlit(true)"),
|
||||
"{rel} n'arme plus set_unlit(true) : la promesse « unlit » du bandeau de `manual` n'est plus garantie par les deux exemples"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,13 +133,27 @@ fn la_facade_delegue_au_meme_render_scene_que_la_voie_manuelle() {
|
||||
.lines()
|
||||
.filter(|l| l.contains("renderer.render_scene("))
|
||||
.collect();
|
||||
assert_eq!(delegations.len(), 1, "la facade a {} appels à `renderer.render_scene(` : deux voies de rendu dans `App`, et le désaccord promis par « identical output from two independent code paths » aurait où se cacher", delegations.len());
|
||||
assert_eq!(
|
||||
delegations.len(),
|
||||
1,
|
||||
"la facade a {} appels à `renderer.render_scene(` : deux voies de rendu dans `App`, et le désaccord promis par « identical output from two independent code paths » aurait où se cacher",
|
||||
delegations.len()
|
||||
);
|
||||
|
||||
let manuel = strip_comments(&lire("examples/meshes/manual.rs"));
|
||||
assert!(manuel.contains("Renderer::new("), "l'exemple manuel ne construit plus le renderer à la main : ce n'est plus la voie sans facade");
|
||||
assert!(manuel.contains("renderer.render("), "l'exemple manuel n'appelle plus l'entrée `Renderer::render` : la confrontation des deux voies ne porte plus sur la même fonction — et le bandeau de `manual` décrit une voie qui n'existe plus");
|
||||
assert!(
|
||||
manuel.contains("Renderer::new("),
|
||||
"l'exemple manuel ne construit plus le renderer à la main : ce n'est plus la voie sans facade"
|
||||
);
|
||||
assert!(
|
||||
manuel.contains("renderer.render("),
|
||||
"l'exemple manuel n'appelle plus l'entrée `Renderer::render` : la confrontation des deux voies ne porte plus sur la même fonction — et le bandeau de `manual` décrit une voie qui n'existe plus"
|
||||
);
|
||||
let renderer_src = strip_comments(&lire("src/core/renderer.rs"));
|
||||
assert!(renderer_src.contains("pub fn render(") && renderer_src.contains("pub fn render_scene("), "`Renderer` n'expose plus les deux entrées à la fois : le bandeau de `manual` (voie manuelle contre voie facade) ne décrit plus deux chemins réels");
|
||||
assert!(
|
||||
renderer_src.contains("pub fn render(") && renderer_src.contains("pub fn render_scene("),
|
||||
"`Renderer` n'expose plus les deux entrées à la fois : le bandeau de `manual` (voie manuelle contre voie facade) ne décrit plus deux chemins réels"
|
||||
);
|
||||
}
|
||||
|
||||
/// Caractérisation (famille `log_depth_drops_triangles_spanning_a_large_depth_range`) : elle rougira
|
||||
@@ -127,6 +163,12 @@ fn la_facade_delegue_au_meme_render_scene_que_la_voie_manuelle() {
|
||||
fn la_divergence_de_normales_entre_simple_et_manuel_est_consignee() {
|
||||
let simple = strip_comments(&lire("examples/meshes/simple.rs"));
|
||||
let manuel = strip_comments(&lire("examples/meshes/manual.rs"));
|
||||
assert!(simple.contains(".with_normals("), "`simple` ne fournit plus de normales : la divergence décrite ici n'existe plus, relire les deux exemples");
|
||||
assert!(!manuel.contains(".with_normals("), "`manual` fournit désormais des normales : la divergence est résorbée — relire le bandeau et le gotcha 15.5 avant de retirer ce verrou");
|
||||
assert!(
|
||||
simple.contains(".with_normals("),
|
||||
"`simple` ne fournit plus de normales : la divergence décrite ici n'existe plus, relire les deux exemples"
|
||||
);
|
||||
assert!(
|
||||
!manuel.contains(".with_normals("),
|
||||
"`manual` fournit désormais des normales : la divergence est résorbée — relire le bandeau et le gotcha 15.5 avant de retirer ce verrou"
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user