Draw tfrag per-vertex colour, unlit, and record why the frame is dark
The tfrag RGBA array now modulates the texture. It is indexed by *position*, not by vertex-info: several vertex-info entries can share a position, and the array is sized vert_count, which is a position count. Colour follows the same rule as the vertex itself. Morphing blends the position only -- the game keeps the high-detail colour -- while collapse replaces the vertex outright, colour included. Getting that the other way round would tint distant terrain. The material is unlit, and that is a decision rather than a shortcut. Level 0's colours really are dark (patch 0 runs (25,26,26) to (108,144,141)) and the game's lighting is what brightens them, so what is on screen is the true albedo. Worth recording: the obvious move -- add a directional light, or crank ambient brightness -- does nothing at all. With no normals the PBR path is degenerate, so the brightness change has no effect, and a host light would have hidden the gap rather than shown it. The colour data was always reaching the mesh; what is missing is lighting, not a material flag. Written up in plan/level-assets.md, including what LightTfrags actually needs: the light bank from the level's gameplay extent, the 256-entry normal table at BOOT_NORMAL_TABLE_VADDR (0x165500) which every level overlay carries, and the already-parsed azimuth/elevation light records.
This commit is contained in:
@@ -66,6 +66,7 @@ pub struct PatchBounds {
|
||||
pub struct TriangleBatch {
|
||||
pub positions: Vec<[f32; 3]>,
|
||||
pub uvs: Vec<[f32; 2]>,
|
||||
pub colors: Vec<[f32; 4]>,
|
||||
/// Three vertex indices per triangle.
|
||||
pub indices: Vec<u32>,
|
||||
/// The ad-gif in force per triangle, parallel to `indices`.
|
||||
@@ -170,6 +171,31 @@ pub fn uv(g: &TfragGeometry, vinfo: usize) -> [f32; 2] {
|
||||
pub struct ResolvedVertex {
|
||||
pub position: [f32; 3],
|
||||
pub uv: [f32; 2],
|
||||
/// The patch's stored per-vertex colour.
|
||||
///
|
||||
/// Indexed by *position*, not by vertex-info: several vertex-info entries
|
||||
/// can share a position, and the colour array is sized `vert_count`, which
|
||||
/// is a position count. This is the game's base colour before lighting
|
||||
/// multiplies into it, so on its own it is the unlit albedo.
|
||||
pub color: [f32; 4],
|
||||
}
|
||||
|
||||
/// The stored colour of a vertex's position, or opaque white if absent.
|
||||
pub fn vertex_color(g: &TfragGeometry, vinfo: usize) -> [f32; 4] {
|
||||
let i = g.position_index(vinfo);
|
||||
match g.rgba.get(i) {
|
||||
// The disc's 0x80-is-1.0 alpha convention, same as the texture CLUT.
|
||||
Some(c) => {
|
||||
let a = if c.a < 0x80 { c.a as f32 / 128.0 } else { 1.0 };
|
||||
[
|
||||
c.r as f32 / 255.0,
|
||||
c.g as f32 / 255.0,
|
||||
c.b as f32 / 255.0,
|
||||
a,
|
||||
]
|
||||
}
|
||||
None => [1.0, 1.0, 1.0, 1.0],
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolve one vertex-info entry at a given view depth.
|
||||
@@ -190,6 +216,7 @@ pub fn resolve_vertex(
|
||||
return ResolvedVertex {
|
||||
position: world(g, vinfo),
|
||||
uv: uv(g, vinfo),
|
||||
color: vertex_color(g, vinfo),
|
||||
};
|
||||
};
|
||||
let limit = link.tier.collapse_distance(lod_distances);
|
||||
@@ -197,12 +224,15 @@ pub fn resolve_vertex(
|
||||
return ResolvedVertex {
|
||||
position: world(g, link.parent1_vinfo),
|
||||
uv: uv(g, link.parent1_vinfo),
|
||||
// Collapse replaces the vertex outright, colour included.
|
||||
color: vertex_color(g, link.parent1_vinfo),
|
||||
};
|
||||
}
|
||||
if !link.morphs {
|
||||
return ResolvedVertex {
|
||||
position: world(g, vinfo),
|
||||
uv: uv(g, vinfo),
|
||||
color: vertex_color(g, vinfo),
|
||||
};
|
||||
}
|
||||
let u = link.tier.weight(depth, lod_distances);
|
||||
@@ -210,6 +240,7 @@ pub fn resolve_vertex(
|
||||
return ResolvedVertex {
|
||||
position: world(g, vinfo),
|
||||
uv: uv(g, vinfo),
|
||||
color: vertex_color(g, vinfo),
|
||||
};
|
||||
}
|
||||
let p1 = raw_position(g, link.parent1_vinfo);
|
||||
@@ -225,10 +256,12 @@ pub fn resolve_vertex(
|
||||
here[1] + (mid[1] - here[1]) * u,
|
||||
here[2] + (mid[2] - here[2]) * u,
|
||||
];
|
||||
// UVs are not morphed: the game keeps the high-detail texture coordinate.
|
||||
// UVs and colours are not morphed: the blend is positional only, and the
|
||||
// game keeps the high-detail texture coordinate and colour.
|
||||
ResolvedVertex {
|
||||
position: to_world(blended),
|
||||
uv: uv(g, vinfo),
|
||||
color: vertex_color(g, vinfo),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -316,6 +349,7 @@ pub fn build_level(src: &LevelSource<'_>, camera: [f32; 3]) -> anyhow::Result<Le
|
||||
.or_insert_with(|| TriangleBatch {
|
||||
positions: Vec::new(),
|
||||
uvs: Vec::new(),
|
||||
colors: Vec::new(),
|
||||
indices: Vec::new(),
|
||||
ad_gifs: Vec::new(),
|
||||
});
|
||||
@@ -324,6 +358,7 @@ pub fn build_level(src: &LevelSource<'_>, camera: [f32; 3]) -> anyhow::Result<Le
|
||||
let r = resolve_vertex(&g, v, depth, lod_distances, collapse);
|
||||
batch.positions.push(r.position);
|
||||
batch.uvs.push(r.uv);
|
||||
batch.colors.push(r.color);
|
||||
}
|
||||
batch.indices.extend_from_slice(&[base, base + 1, base + 2]);
|
||||
batch.ad_gifs.push(*ad);
|
||||
|
||||
@@ -388,6 +388,7 @@ fn mesh_of(b: &TriangleBatch) -> Option<Mesh> {
|
||||
);
|
||||
mesh.insert_attribute(Mesh::ATTRIBUTE_POSITION, b.positions.clone());
|
||||
mesh.insert_attribute(Mesh::ATTRIBUTE_UV_0, b.uvs.clone());
|
||||
mesh.insert_attribute(Mesh::ATTRIBUTE_COLOR, b.colors.clone());
|
||||
// No normals and no colours: lighting is the game's own per-vertex light
|
||||
// array, which is parsed but not yet reproduced. Inventing normals would
|
||||
// make unlit terrain look lit and hide that gap.
|
||||
|
||||
Reference in New Issue
Block a user