✦ SPIELWELTENMoto Racer - Rampage ↗

Individual fir card shading basis

The native Windows views in QA/20260915-195454-campaign/alps-01-pass0-view0.png and alps-01-pass0-view1.png show pale, flat distant fir crowns. The large near mesh fir at the right edge of view1 already has substantially darker branches and spatial depth. The corrected original-UV summer atlas was inspected again: its A/B crowns contain dark green photographed needles and fine transparent gaps. Its SHA256 remains 4b8cc73edb0965a42d6268ed513e00028026cd9f1c6b0ded17f2767aabd07b23.

Proven source fault and bounded correction

AlpineFirAssets.Card previously assigned Vector3.up to all eight vertices of the two vertical planes. The shading normal was perpendicular to the geometric plane normal. With a camera near the ground, nearly every crown fragment consequently had a negative dot product between the shading normal and the direction towards the camera (NdotV). HDRP's Lit lighting precomputation uses this dot product and then clamps it. The basis was demonstrably incorrect; the exact contribution to the native brightness is not established by this geometric analysis alone.

The published correction assigns each plane its actual normal, Cross(Vector3.up, right), matching the unchanged triangle winding. Individual fir impostor materials use HDRP's double-sided Flip mode so the visible back side receives the opposite plane normal. There is no upward vector to invert into a downward vector. The normal map remains absent, and the material's original photographed color, alpha, zero smoothness and cutoff remain unchanged. Summer and winter individual cards use the same geometry fix with their existing separate atlas layouts.

Package evidence is local HDRP 700710090fa9: Runtime/Material/MaterialUtilities.hlsl applies the front/back sign to tangentToWorld[2]; Runtime/Material/Lit/BaseLitAPI.cs maps Flip to (-1,-1,-1,0); Runtime/Material/Lit/Lit.hlsl computes and clamps NdotV. HDMaterial.ValidateMaterial runs after setting the correct mode. No package file is modified.

CPU evidence and geometry budget

Reproduce with Tools/Art/analyze_fir_impostor_shading.py. Actual results are in ArtSource/Alps/FirImpostorStudy/normal-study.json. The study samples the original A/B atlas alpha, both crossed views, 24 camera azimuths and distances of 80, 160 and 320 m. It uses a 1.5 m camera above level ground and weights passing-alpha samples by their projected plane area. This controlled geometry study does not replay a native drive and does not render HDRP.

Measurement Previous upward normals Correct double-sided plane normals
Weighted samples with NdotV ≤ 0, all three distances 98.46% 0%
Median NdotV at 80 m −0.139 +0.857
Median NdotV at 160 m −0.070 +0.864
Median NdotV at 320 m −0.035 +0.866
Vertices / triangles per individual tree 8 / 4 8 / 4
Additional mesh data 0

A rounded alternative with explicit front/back vertices was also evaluated but not published. It doubled the tree card geometry to 16 vertices / 8 triangles, still gave slightly negative NdotV at some oblique edges, and would add roughly 46.9 MB of raw mesh channels at the recorded Alps02 population of 108,556 individual cards, assuming 32-bit indices and tangents. The selected correction has no such memory increase. Triangle indices, bounds, UVs, winding, alpha cutoff, atlas files, LOD thresholds, batching, placement and mesh ownership remain intact.

Validation boundary and remaining issues

The managed Runtime + Editor compilation at 2026-09-15 18:43:41 UTC passes with 92 + 21 files, zero warnings/errors. Its complete source-hash record is archived beside the study as managed-compile.json. The published AlpineFirAssets.cs file hash is 798253cd50e9a2d54037138059646f8e817e5dab0bfdda8c04b5347d5e1b1627 (raw bytes). No Unity or player process was started for this work. The next root-owned native build must compare the same driving poses and check both card sides for new black/white inversions, visible plane edges and remaining pale crowns. This is not a native visual acceptance or an AAA claim.

The atlas includes lighting from its original CPU bake; correcting runtime normals cannot remove that baked illumination. AlpineForestCanopy still places some individual cards close enough to reveal their flat geometry, and repeated A/B silhouettes remain visible. Promoting thousands of trees to 3D meshes would create a separate substantial geometry cost and was not done here. The separate 15-tree ForestCluster path, currently used for distant winter groups, retains its existing upward-normal behavior and was deliberately outside this individual-card change. Its lighting and winter forest cohesion need separate native evaluation.

Native follow-up: graphics acceptance failed

The subsequent 20260915-210513 native Alps01 view0/view1 images were personally compared with 195454. The uniformly pale crowns are reduced, but alternating dark/bright plane halves and edge-on slices are conspicuous in the middle forest of view1 and along the right hillside. The nearby 3D fir retains actual branch depth. The geometrically correct normal change therefore does not pass the realism requirement. Reverting to the mathematically invalid upward normals would merely restore the earlier pale-card artifact.

The two-plane construction and lighting representation must change together for a substantial improvement. The atlas is a Cycles beauty render, containing photographed foliage plus source illumination; prepare_fir.py uses sky strength 0.8, sun energy 1.8 and the Standard view transform for an LDR PNG. Treating that beauty image as a physically neutral albedo and illuminating each vertical plane again with HDRP/Lit creates an orientation-dependent second lighting layer. The native images are consistent with this mechanism. This is distinct from a source-UV failure or a missing texture.

Use one vertical, GPU camera-facing whole-tree quad per far individual fir, with a baked-lighting material and the existing photographed alpha. Cylindrical billboarding keeps the trunk vertical and rooted at its current terrain position; it must operate per-tree within a batch, not rotate an entire forest batch or update 100,000 GameObjects on the CPU. One quad is 4 vertices / 2 triangles, versus the current crossed-card 8 / 4. Preserve a full A/B silhouette rather than showing the two orthographic views as simultaneous intersecting planes. Current near 3D meshes should remain actual meshes. A material-only change to the existing crosses would remove directional checkerboarding, but leave obvious slices and intersecting crowns.

The local HDRP source supports the needed baked-lighting behavior:

The existing LDR beauty atlas has no absolute radiance calibration, so an arbitrary multiplier must not be called a physical bake intensity. A small calibration using an 18% neutral reference under the exact source bake lighting, retaining unclipped linear output, can establish a documented conversion; a native same-pose comparison must then validate brightness against the nearby 3D fir. For a fixed daylight environment this is a bounded approximation, not dynamic relighting of a full 3D tree. Standard HDRP/Unlit also has no built-in per-tree camera-facing vertex behavior; that needs a small dedicated billboard shader/graph with matched depth and motion-vector positions and conservative per-tree/batch bounds.

Billboarding does not make a nearby flat image photoreal. At a 65-degree vertical field of view and 1080 pixels high, a 19.7 m tree is approximately 209 pixels high at 80 m, 104 at 160 m and 52 at 320 m. The current Far placement starts in the visibly near/middle range. Its near transition remains a separate budgeted 3D-LOD problem; this recommendation does not promote thousands of existing high-triangle meshes or claim that a single unlit material resolves all forest realism defects.

This follow-up changed documentation only. No new shader, material, atlas or runtime billboard implementation was published, and no Unity/player was started by the investigator.

Implemented GPU whole-tree follow-up (native acceptance pending)

Following the native rejection and the next implementation authorization, the individual-tree path now uses one cylindrical GPU billboard. AlpineFirBillboards.cs supplies a shared four-vertex / two-triangle mesh for each near-tree LOD2 variant and spatial batches for Far. Each vertex stores the tree's ground pivot in POSITION, the unchanged atlas coordinates in UV0 and its horizontal/vertical metre offset in UV1. The shader expands each pivot independently towards the current camera while keeping its trunk vertical. There are no per-tree CPU updates, compute kernels or structured buffers. The only WorldGeometry.cs change is a dedicated Fir batch collector/emitter alongside the existing batches; other geometry is untouched.

The original Sommer/Winter A/B atlases and their two recorded views remain in place, with a 0.5 frame width/height ratio. A deterministic source view is selected per tree; both views are no longer rendered as intersecting planes. All original ground positions, requested heights, collision geometry and road surfaces remain unchanged. LOD0/LOD1 meshes, branch snow meshes and LOD thresholds are retained. The separate distant winter ForestCluster material/geometry is still outside this change.

The shader lives under Resources/Shaders/AlpineFirBillboard.shader, ensuring an actual Resources asset reference, and existing MaterialLibrary reference enumeration now returns its summer and winter representative materials through AlpineFirAssets. The build configurator saves those actual materials and their cutout/emission keywords. This is stronger retention than relying on runtime shader-name lookup.

Tools/Art/prepare_fir_billboard_shader.py reproducibly extracts the pinned HDRP Unlit DepthForwardOnly, MotionVectors and ForwardOnly passes. Its original source SHA256 is 0e35c80cb2a7979bb4d289b7c3e12738544df83eb0df9a2c42eee6372952ebbd. Shared AlpineFirBillboardGeometry.hlsl applies the same expansion to every current-camera pass. AlpineFirBillboardMotion.hlsl additionally expands the previous position using HDRP's previous camera in the current relative coordinate system, then uses the package's usual motion encoding. It does not infer the previous frame from game time, so paused-time camera movement remains well-defined. Motion-vector rendering is explicitly enabled; ordinary stock-Unlit validation would disable vertex-deformation vectors.

The material retains opaque alpha clipping at 0.24 and uses the original alpha source. Its zero base RGB and exposure-weighted emissive image implement baked lighting without a second directional Lit pass. The initial authored exposure calibration is 1 / ColorUtils.ConvertEV100ToExposure(13) = 9830.4 with HDRP's default 0.65 lens attenuation. This preserves source-linear RGB approximately at reference EV13 before the game's tone curve. It is not an absolute physical luminance measurement and still needs comparison against nearby 3D firs in the native game. No texture recoloring, saturation multiplier, emission GI or bloom boost was introduced. Opaque depth-based atmospheric scattering remains the native fog path.

Tools/Art/verify_fir_billboard_geometry.py passes 9,720 analytic camera/pivot/scale configurations, checking vertical base contact, full frame dimensions, all-angle conservative bounds, front winding and previous-camera reconstruction. Its maximum double-precision camera-relative reconstruction error is 9.095e-13 m. Summer/winter image hashes are unchanged. Mesh bounds explicitly include every yaw of the expanded quad; using the degenerate input-pivot bounds would otherwise hide visible trees. The shared and scene-generated mesh ownership remains separated, with generated cells owned by GeneratedWorldResources.

Managed compilation passes 93 Runtime + 22 Editor files, zero warnings/errors, archived as ArtSource/Alps/FirImpostorStudy/billboard-managed-compile.json. The corresponding geometry report is billboard-geometry-verification.json; shader generation provenance is billboard-shader-generation.json.

The root-owned native entry point Rampage.Editor.FirBillboardVerification.Run() compiles the three actual passes synchronously and records shader errors, source-material retention, enabled motion vectors and imported quad bounds in Docs/QA/fir-billboard-verification.json. It must run before the next native capture. Neither the managed check nor the analytic study is a shader-compilation or graphics pass. Final checks remain: no pink/missing trees, coherent brightness without crossed dark sides, stable silhouettes while steering, correct fog/depth and LOD transitions, and memory plateau after revisiting stages. No Unity/player was started by this implementation work.