BlueGit

golden-age paperdoll-renderer

Merge feature/khr-specular (Tim t998u.4)

timfa Grok Bot · pushed by timfa

Authored 2026-11-07 · 6 days ago

Hash
62b79b7d08b3f0ef5982c03692f37d3e38e83893
Pipeline
Passed
10 files changed +179 −49
Cargo.toml +1 −1
@@ -10,7 +10,7 @@ description = "Paperdoll Renderer with project files, model import and a live gl
10 10 eframe = { version = "0.29.1", default-features = false, features = ["default_fonts", "glow", "x11", "wayland"] }
11 11 egui_glow = "0.29.1"
12 12 glow = "0.14.2"
13 gltf = { version = "1.4.1", default-features = false, features = ["import", "names", "utils", "KHR_texture_transform"] }
13 gltf = { version = "1.4.1", default-features = false, features = ["import", "names", "utils", "KHR_texture_transform", "KHR_materials_specular"] }
14 14 libloading = "0.8"
15 15 png = "0.18"
16 16 rfd = "0.15.4"
@@ -10,7 +10,7 @@ description = "Paperdoll Renderer with project files, model import and a live gl
10eframe = { version = "0.29.1", default-features = false, features = ["default_fonts", "glow", "x11", "wayland"] }
10eframe = { version = "0.29.1", default-features = false, features = ["default_fonts", "glow", "x11", "wayland"] }
11egui_glow = "0.29.1"
11egui_glow = "0.29.1"
12glow = "0.14.2"
12glow = "0.14.2"
13gltf = { version = "1.4.1", default-features = false, features = ["import", "names", "utils", "KHR_texture_transform"] }
13gltf = { version = "1.4.1", default-features = false, features = ["import", "names", "utils", "KHR_texture_transform", "KHR_materials_specular"] }
14libloading = "0.8"
14libloading = "0.8"
15png = "0.18"
15png = "0.18"
16rfd = "0.15.4"
16rfd = "0.15.4"
README.md +4 −4
@@ -36,7 +36,7 @@ The first project view shows the bodies. **Choose male / Choose female** selects
36 36
37 37 ### Multiple meshes and skins (glTF/GLB)
38 38
39 A skinned glTF/GLB body is loaded completely: all mesh nodes and all skins. The joints of all skins are merged into one skeleton (joints present in several skins count once; at most 128). A mesh node without a skin moves rigidly with the nearest ancestor that is a joint; if it has none, it hangs from the first joint. Its rest position in the scene is kept exactly (mirrored transforms flip the triangle winding). Materials stay per primitive: base color factor × albedo texture, alpha cutoff and vertex colors; every primitive uses its own material, in the live preview and in production. External image files (`uri` next to the `.gltf`) are resolved relative to the model, like embedded and `data:` images. `KHR_texture_transform` on the base color texture (offset, rotation, scale and `texCoord`) is applied once to the UVs on load. The sampler wrap mode is not read: textures are always sampled nearest with REPEAT, so CLAMP/MIRROR look different outside the 0–1 UV range. Metallic/roughness, normal, occlusion and emissive textures and morph targets are not used; the normal map comes from geometry and a body's specular map is zero (metallic/roughness is not converted; a scalar specular cannot reproduce PBR physically).
39 A skinned glTF/GLB body is loaded completely: all mesh nodes and all skins. The joints of all skins are merged into one skeleton (joints present in several skins count once; at most 128). A mesh node without a skin moves rigidly with the nearest ancestor that is a joint; if it has none, it hangs from the first joint. Its rest position in the scene is kept exactly (mirrored transforms flip the triangle winding). Materials stay per primitive: base color factor × albedo texture, alpha cutoff and vertex colors; every primitive uses its own material, in the live preview and in production. External image files (`uri` next to the `.gltf`) are resolved relative to the model, like embedded and `data:` images. `KHR_texture_transform` on the base color texture (offset, rotation, scale and `texCoord`) is applied once to the UVs on load. The sampler wrap mode is not read: textures are always sampled nearest with REPEAT, so CLAMP/MIRROR look different outside the 0–1 UV range. `KHR_materials_specular` feeds the body's specular map per primitive: `specularFactor` × the alpha of `specularTexture` (sampled with the base-colour UVs; `specularColor*` is not used); in the live preview it drives the highlight. Without the extension a body's specular is zero. Metallic/roughness, normal, occlusion and emissive textures and morph targets are not used; the normal map comes from geometry (metallic/roughness is not converted; a scalar specular cannot reproduce PBR physically).
40 40
41 41 ### Static bodies (OBJ, glTF/FBX without skin)
42 42
@@ -115,7 +115,7 @@ The depth format is chosen in the Body screen (**Depth map**), stored in the `.p
115 115 - `gray` (default): gray+alpha, `gray = round(t·M)`.
116 116 - `rgb_bands`: RGBA, `v = round(t·3M)`, R = min(v, M), G = clamp(v − M, 0, M), B = clamp(v − 2M, 0, M); t = (R+G+B)/3M (766 levels at 8 bit).
117 117 - `rgb_packed`: RGBA, positional in base 2^bits (24-bit or 48-bit value), R most significant; needs nearest sampling, no mipmaps and lossless storage.
118 Alpha is kept (scaled to the bit depth); transparent pixels have value 0. Geometry outside the range is clamped (not clipped) and reported in the progress; normal is view-space RGB+alpha; specular is linear strength (body zero, equipment including the configured shiny map). Highres and detail are visible intermediate steps only and are not stored.
118 Alpha is kept (scaled to the bit depth); transparent pixels have value 0. Geometry outside the range is clamped (not clipped) and reported in the progress; normal is view-space RGB+alpha; specular is linear strength (body: KHR_materials_specular or zero; equipment including the configured shiny map). Highres and detail are visible intermediate steps only and are not stored.
119 119
120 120 ```text
121 121 renders/male-body-atlas/raw files/male/body/clip001-walk/f000012/p-60_y330_diffuse.png
@@ -221,8 +221,8 @@ xvfb-run -a cargo run --release --example resume_fixture --locked -- resume /tmp
221 221 cargo run --release --example resume_fixture --locked -- verify /tmp/resume
222 222 # FBX on a real GPU: live view (body + FBX sword) and full production of body and equipment set
223 223 xvfb-run -a cargo run --example fbx_live_fixture --locked -- /tmp/fbx-live /tmp/fbx-live-proof 32 48 4
224 # Multi-material glTF on a real GPU: external .bin + PNG textures, three materials (factor, texture, texture × factor);
225 # screenshots the live view and checks every material colour in the production diffuse
224 # Multi-material glTF on a real GPU: external .bin + PNG textures, three materials (factor, texture, texture × factor, plus KHR_materials_specular);
225 # screenshots the live view and checks every material colour in the production diffuse and the specular strengths
226 226 xvfb-run -a -s '-screen 0 1280x1024x24' cargo run --release --example textured_body_fixture --locked -- /tmp/tex /tmp/tex-proof tests/fixtures/bodies/multi-material body.gltf --expect-multi-material
227 227 # Depth range check on the CPU: per pitch the depth extent of every clip frame and view versus near/far
228 228 cargo run --release --example depth_clamp_probe --locked -- tests/fixtures/bodies/test-body-male.glb 2600 1000 4
@@ -36,7 +36,7 @@ The first project view shows the bodies. **Choose male / Choose female** selects
36
36
37### Multiple meshes and skins (glTF/GLB)
37### Multiple meshes and skins (glTF/GLB)
38
38
39A skinned glTF/GLB body is loaded completely: all mesh nodes and all skins. The joints of all skins are merged into one skeleton (joints present in several skins count once; at most 128). A mesh node without a skin moves rigidly with the nearest ancestor that is a joint; if it has none, it hangs from the first joint. Its rest position in the scene is kept exactly (mirrored transforms flip the triangle winding). Materials stay per primitive: base color factor × albedo texture, alpha cutoff and vertex colors; every primitive uses its own material, in the live preview and in production. External image files (`uri` next to the `.gltf`) are resolved relative to the model, like embedded and `data:` images. `KHR_texture_transform` on the base color texture (offset, rotation, scale and `texCoord`) is applied once to the UVs on load. The sampler wrap mode is not read: textures are always sampled nearest with REPEAT, so CLAMP/MIRROR look different outside the 0–1 UV range. Metallic/roughness, normal, occlusion and emissive textures and morph targets are not used; the normal map comes from geometry and a body's specular map is zero (metallic/roughness is not converted; a scalar specular cannot reproduce PBR physically).
39A skinned glTF/GLB body is loaded completely: all mesh nodes and all skins. The joints of all skins are merged into one skeleton (joints present in several skins count once; at most 128). A mesh node without a skin moves rigidly with the nearest ancestor that is a joint; if it has none, it hangs from the first joint. Its rest position in the scene is kept exactly (mirrored transforms flip the triangle winding). Materials stay per primitive: base color factor × albedo texture, alpha cutoff and vertex colors; every primitive uses its own material, in the live preview and in production. External image files (`uri` next to the `.gltf`) are resolved relative to the model, like embedded and `data:` images. `KHR_texture_transform` on the base color texture (offset, rotation, scale and `texCoord`) is applied once to the UVs on load. The sampler wrap mode is not read: textures are always sampled nearest with REPEAT, so CLAMP/MIRROR look different outside the 0–1 UV range. `KHR_materials_specular` feeds the body's specular map per primitive: `specularFactor` × the alpha of `specularTexture` (sampled with the base-colour UVs; `specularColor*` is not used); in the live preview it drives the highlight. Without the extension a body's specular is zero. Metallic/roughness, normal, occlusion and emissive textures and morph targets are not used; the normal map comes from geometry (metallic/roughness is not converted; a scalar specular cannot reproduce PBR physically).
40
40
41### Static bodies (OBJ, glTF/FBX without skin)
41### Static bodies (OBJ, glTF/FBX without skin)
42
42
@@ -115,7 +115,7 @@ The depth format is chosen in the Body screen (**Depth map**), stored in the `.p
115- `gray` (default): gray+alpha, `gray = round(t·M)`.
115- `gray` (default): gray+alpha, `gray = round(t·M)`.
116- `rgb_bands`: RGBA, `v = round(t·3M)`, R = min(v, M), G = clamp(v − M, 0, M), B = clamp(v − 2M, 0, M); t = (R+G+B)/3M (766 levels at 8 bit).
116- `rgb_bands`: RGBA, `v = round(t·3M)`, R = min(v, M), G = clamp(v − M, 0, M), B = clamp(v − 2M, 0, M); t = (R+G+B)/3M (766 levels at 8 bit).
117- `rgb_packed`: RGBA, positional in base 2^bits (24-bit or 48-bit value), R most significant; needs nearest sampling, no mipmaps and lossless storage.
117- `rgb_packed`: RGBA, positional in base 2^bits (24-bit or 48-bit value), R most significant; needs nearest sampling, no mipmaps and lossless storage.
118Alpha is kept (scaled to the bit depth); transparent pixels have value 0. Geometry outside the range is clamped (not clipped) and reported in the progress; normal is view-space RGB+alpha; specular is linear strength (body zero, equipment including the configured shiny map). Highres and detail are visible intermediate steps only and are not stored.
118Alpha is kept (scaled to the bit depth); transparent pixels have value 0. Geometry outside the range is clamped (not clipped) and reported in the progress; normal is view-space RGB+alpha; specular is linear strength (body: KHR_materials_specular or zero; equipment including the configured shiny map). Highres and detail are visible intermediate steps only and are not stored.
119
119
120```text
120```text
121renders/male-body-atlas/raw files/male/body/clip001-walk/f000012/p-60_y330_diffuse.png
121renders/male-body-atlas/raw files/male/body/clip001-walk/f000012/p-60_y330_diffuse.png
@@ -221,8 +221,8 @@ xvfb-run -a cargo run --release --example resume_fixture --locked -- resume /tmp
221cargo run --release --example resume_fixture --locked -- verify /tmp/resume
221cargo run --release --example resume_fixture --locked -- verify /tmp/resume
222# FBX on a real GPU: live view (body + FBX sword) and full production of body and equipment set
222# FBX on a real GPU: live view (body + FBX sword) and full production of body and equipment set
223xvfb-run -a cargo run --example fbx_live_fixture --locked -- /tmp/fbx-live /tmp/fbx-live-proof 32 48 4
223xvfb-run -a cargo run --example fbx_live_fixture --locked -- /tmp/fbx-live /tmp/fbx-live-proof 32 48 4
224# Multi-material glTF on a real GPU: external .bin + PNG textures, three materials (factor, texture, texture × factor);
224# Multi-material glTF on a real GPU: external .bin + PNG textures, three materials (factor, texture, texture × factor, plus KHR_materials_specular);
225# screenshots the live view and checks every material colour in the production diffuse
225# screenshots the live view and checks every material colour in the production diffuse and the specular strengths
226xvfb-run -a -s '-screen 0 1280x1024x24' cargo run --release --example textured_body_fixture --locked -- /tmp/tex /tmp/tex-proof tests/fixtures/bodies/multi-material body.gltf --expect-multi-material
226xvfb-run -a -s '-screen 0 1280x1024x24' cargo run --release --example textured_body_fixture --locked -- /tmp/tex /tmp/tex-proof tests/fixtures/bodies/multi-material body.gltf --expect-multi-material
227# Depth range check on the CPU: per pitch the depth extent of every clip frame and view versus near/far
227# Depth range check on the CPU: per pitch the depth extent of every clip frame and view versus near/far
228cargo run --release --example depth_clamp_probe --locked -- tests/fixtures/bodies/test-body-male.glb 2600 1000 4
228cargo run --release --example depth_clamp_probe --locked -- tests/fixtures/bodies/test-body-male.glb 2600 1000 4
examples/textured_body_fixture.rs +20 −0
@@ -219,6 +219,26 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
219 219 return Err(format!("production diffuse mist materiaal {name}").into());
220 220 }
221 221 }
222 // KHR_materials_specular: specular map strengths 0, 0.5 and 0.4.
223 let spec_path = front.with_file_name("p+00_y000_specular.png");
224 let decoder = png::Decoder::new(std::io::BufReader::new(std::fs::File::open(&spec_path)?));
225 let mut reader = decoder.read_info()?;
226 let mut spec = vec![0; reader.output_buffer_size().ok_or("png size")?];
227 let info = reader.next_frame(&mut spec)?;
228 let c = spec.len() / (info.width as usize * info.height as usize);
229 let mut values: Vec<u8> = spec
230 .chunks_exact(c)
231 .filter(|p| p[c - 1] == 255)
232 .map(|p| p[0])
233 .collect();
234 values.sort();
235 values.dedup();
236 println!("SPECULAR values {values:?}");
237 for want in [0u8, 128, 102] {
238 if !values.iter().any(|v| v.abs_diff(want) <= 2) {
239 return Err(format!("specular map mist sterkte {want}").into());
240 }
241 }
222 242 }
223 243 Ok(())
224 244 }
@@ -219,6 +219,26 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
219 return Err(format!("production diffuse mist materiaal {name}").into());
219 return Err(format!("production diffuse mist materiaal {name}").into());
220 }
220 }
221 }
221 }
222 // KHR_materials_specular: specular map strengths 0, 0.5 and 0.4.
223 let spec_path = front.with_file_name("p+00_y000_specular.png");
224 let decoder = png::Decoder::new(std::io::BufReader::new(std::fs::File::open(&spec_path)?));
225 let mut reader = decoder.read_info()?;
226 let mut spec = vec![0; reader.output_buffer_size().ok_or("png size")?];
227 let info = reader.next_frame(&mut spec)?;
228 let c = spec.len() / (info.width as usize * info.height as usize);
229 let mut values: Vec<u8> = spec
230 .chunks_exact(c)
231 .filter(|p| p[c - 1] == 255)
232 .map(|p| p[0])
233 .collect();
234 values.sort();
235 values.dedup();
236 println!("SPECULAR values {values:?}");
237 for want in [0u8, 128, 102] {
238 if !values.iter().any(|v| v.abs_diff(want) <= 2) {
239 return Err(format!("specular map mist sterkte {want}").into());
240 }
241 }
222 }
242 }
223 Ok(())
243 Ok(())
224}
244}
scripts/generate_multi_material_body.py +14 −7
@@ -1,7 +1,9 @@
1 1 #!/usr/bin/env python3
2 """CC0 regression fixture: a skinned glTF body with an external .bin, two
2 """CC0 regression fixture: a skinned glTF body with an external .bin, three
3 3 external PNG textures and three materials (factor only, texture, texture x
4 4 factor), one primitive per material. Three quads side by side facing +Z.
5 KHR_materials_specular: Green has specularFactor 0.5; YellowTimesFactor has
6 specularFactor 1 with an RGBA specular texture whose alpha is 102 (0.4).
5 7
6 8 Writes tests/fixtures/bodies/multi-material/{body.gltf,body.bin,textures/*.png}.
7 9 """
@@ -10,11 +12,11 @@ import json, os, struct, zlib
10 12 OUT = os.path.join(os.path.dirname(__file__), "..", "tests", "fixtures", "bodies", "multi-material")
11 13
12 14
13 def png(path, w, h, pixel):
15 def png(path, w, h, pixel, color_type=2):
14 16 raw = b"".join(b"\0" + b"".join(bytes(pixel(x, y)) for x in range(w)) for y in range(h))
15 17 def chunk(t, d):
16 18 return struct.pack(">I", len(d)) + t + d + struct.pack(">I", zlib.crc32(t + d) & 0xFFFFFFFF)
17 data = b"\x89PNG\r\n\x1a\n" + chunk(b"IHDR", struct.pack(">IIBBBBB", w, h, 8, 2, 0, 0, 0))
19 data = b"\x89PNG\r\n\x1a\n" + chunk(b"IHDR", struct.pack(">IIBBBBB", w, h, 8, color_type, 0, 0, 0))
18 20 data += chunk(b"IDAT", zlib.compress(raw)) + chunk(b"IEND", b"")
19 21 with open(path, "wb") as f:
20 22 f.write(data)
@@ -25,6 +27,7 @@ def main():
25 27 # Texture A: solid green. Texture B: solid yellow (multiplied by factor 0.5,1,1).
26 28 png(os.path.join(OUT, "textures", "green_baseColor.png"), 4, 4, lambda x, y: (0, 200, 0))
27 29 png(os.path.join(OUT, "textures", "yellow_baseColor.png"), 4, 4, lambda x, y: (200, 200, 0))
30 png(os.path.join(OUT, "textures", "specular.png"), 4, 4, lambda x, y: (0, 0, 0, 102), color_type=6)
28 31 blob = b""
29 32 views, accessors, prims = [], [], []
30 33
@@ -59,6 +62,7 @@ def main():
59 62 times = accessor(add(struct.pack("<2f", 0.0, 1.0)), 5126, 2, "SCALAR", min=[0.0], max=[1.0])
60 63 rots = accessor(add(struct.pack("<8f", 0, 0, 0, 1, 0, 0, 0, 1)), 5126, 2, "VEC4")
61 64 gltf = {
65 "extensionsUsed": ["KHR_materials_specular"],
62 66 "asset": {"version": "2.0", "generator": "paperdoll generate_multi_material_body.py (CC0)"},
63 67 "extras": {"forward": "+Z"},
64 68 "scene": 0,
@@ -68,13 +72,16 @@ def main():
68 72 "meshes": [{"name": "Body", "primitives": prims}],
69 73 "materials": [
70 74 {"name": "FactorOnly", "pbrMetallicRoughness": {"baseColorFactor": [0.2, 0.4, 0.9, 1.0]}},
71 {"name": "Green", "pbrMetallicRoughness": {"baseColorTexture": {"index": 0}}},
75 {"name": "Green", "pbrMetallicRoughness": {"baseColorTexture": {"index": 0}},
76 "extensions": {"KHR_materials_specular": {"specularFactor": 0.5}}},
72 77 {"name": "YellowTimesFactor", "pbrMetallicRoughness": {"baseColorTexture": {"index": 1},
73 "baseColorFactor": [0.5, 1.0, 1.0, 1.0]}},
78 "baseColorFactor": [0.5, 1.0, 1.0, 1.0]},
79 "extensions": {"KHR_materials_specular": {"specularTexture": {"index": 2}}}},
74 80 ],
75 "textures": [{"sampler": 0, "source": 0}, {"sampler": 0, "source": 1}],
81 "textures": [{"sampler": 0, "source": 0}, {"sampler": 0, "source": 1}, {"sampler": 0, "source": 2}],
76 82 "samplers": [{"magFilter": 9728, "minFilter": 9728}],
77 "images": [{"uri": "textures/green_baseColor.png"}, {"uri": "textures/yellow_baseColor.png"}],
83 "images": [{"uri": "textures/green_baseColor.png"}, {"uri": "textures/yellow_baseColor.png"},
84 {"uri": "textures/specular.png"}],
78 85 "animations": [{"name": "idle", "samplers": [{"input": times, "output": rots}],
79 86 "channels": [{"sampler": 0, "target": {"node": 0, "path": "rotation"}}]}],
80 87 "buffers": [{"uri": "body.bin", "byteLength": len(blob)}],
@@ -1,7 +1,9 @@
1#!/usr/bin/env python3
1#!/usr/bin/env python3
2"""CC0 regression fixture: a skinned glTF body with an external .bin, two
2"""CC0 regression fixture: a skinned glTF body with an external .bin, three
3external PNG textures and three materials (factor only, texture, texture x
3external PNG textures and three materials (factor only, texture, texture x
4factor), one primitive per material. Three quads side by side facing +Z.
4factor), one primitive per material. Three quads side by side facing +Z.
5KHR_materials_specular: Green has specularFactor 0.5; YellowTimesFactor has
6specularFactor 1 with an RGBA specular texture whose alpha is 102 (0.4).
5
7
6Writes tests/fixtures/bodies/multi-material/{body.gltf,body.bin,textures/*.png}.
8Writes tests/fixtures/bodies/multi-material/{body.gltf,body.bin,textures/*.png}.
7"""
9"""
@@ -10,11 +12,11 @@ import json, os, struct, zlib
10OUT = os.path.join(os.path.dirname(__file__), "..", "tests", "fixtures", "bodies", "multi-material")
12OUT = os.path.join(os.path.dirname(__file__), "..", "tests", "fixtures", "bodies", "multi-material")
11
13
12
14
13def png(path, w, h, pixel):
15def png(path, w, h, pixel, color_type=2):
14 raw = b"".join(b"\0" + b"".join(bytes(pixel(x, y)) for x in range(w)) for y in range(h))
16 raw = b"".join(b"\0" + b"".join(bytes(pixel(x, y)) for x in range(w)) for y in range(h))
15 def chunk(t, d):
17 def chunk(t, d):
16 return struct.pack(">I", len(d)) + t + d + struct.pack(">I", zlib.crc32(t + d) & 0xFFFFFFFF)
18 return struct.pack(">I", len(d)) + t + d + struct.pack(">I", zlib.crc32(t + d) & 0xFFFFFFFF)
17 data = b"\x89PNG\r\n\x1a\n" + chunk(b"IHDR", struct.pack(">IIBBBBB", w, h, 8, 2, 0, 0, 0))
19 data = b"\x89PNG\r\n\x1a\n" + chunk(b"IHDR", struct.pack(">IIBBBBB", w, h, 8, color_type, 0, 0, 0))
18 data += chunk(b"IDAT", zlib.compress(raw)) + chunk(b"IEND", b"")
20 data += chunk(b"IDAT", zlib.compress(raw)) + chunk(b"IEND", b"")
19 with open(path, "wb") as f:
21 with open(path, "wb") as f:
20 f.write(data)
22 f.write(data)
@@ -25,6 +27,7 @@ def main():
25 # Texture A: solid green. Texture B: solid yellow (multiplied by factor 0.5,1,1).
27 # Texture A: solid green. Texture B: solid yellow (multiplied by factor 0.5,1,1).
26 png(os.path.join(OUT, "textures", "green_baseColor.png"), 4, 4, lambda x, y: (0, 200, 0))
28 png(os.path.join(OUT, "textures", "green_baseColor.png"), 4, 4, lambda x, y: (0, 200, 0))
27 png(os.path.join(OUT, "textures", "yellow_baseColor.png"), 4, 4, lambda x, y: (200, 200, 0))
29 png(os.path.join(OUT, "textures", "yellow_baseColor.png"), 4, 4, lambda x, y: (200, 200, 0))
30 png(os.path.join(OUT, "textures", "specular.png"), 4, 4, lambda x, y: (0, 0, 0, 102), color_type=6)
28 blob = b""
31 blob = b""
29 views, accessors, prims = [], [], []
32 views, accessors, prims = [], [], []
30
33
@@ -59,6 +62,7 @@ def main():
59 times = accessor(add(struct.pack("<2f", 0.0, 1.0)), 5126, 2, "SCALAR", min=[0.0], max=[1.0])
62 times = accessor(add(struct.pack("<2f", 0.0, 1.0)), 5126, 2, "SCALAR", min=[0.0], max=[1.0])
60 rots = accessor(add(struct.pack("<8f", 0, 0, 0, 1, 0, 0, 0, 1)), 5126, 2, "VEC4")
63 rots = accessor(add(struct.pack("<8f", 0, 0, 0, 1, 0, 0, 0, 1)), 5126, 2, "VEC4")
61 gltf = {
64 gltf = {
65 "extensionsUsed": ["KHR_materials_specular"],
62 "asset": {"version": "2.0", "generator": "paperdoll generate_multi_material_body.py (CC0)"},
66 "asset": {"version": "2.0", "generator": "paperdoll generate_multi_material_body.py (CC0)"},
63 "extras": {"forward": "+Z"},
67 "extras": {"forward": "+Z"},
64 "scene": 0,
68 "scene": 0,
@@ -68,13 +72,16 @@ def main():
68 "meshes": [{"name": "Body", "primitives": prims}],
72 "meshes": [{"name": "Body", "primitives": prims}],
69 "materials": [
73 "materials": [
70 {"name": "FactorOnly", "pbrMetallicRoughness": {"baseColorFactor": [0.2, 0.4, 0.9, 1.0]}},
74 {"name": "FactorOnly", "pbrMetallicRoughness": {"baseColorFactor": [0.2, 0.4, 0.9, 1.0]}},
71 {"name": "Green", "pbrMetallicRoughness": {"baseColorTexture": {"index": 0}}},
75 {"name": "Green", "pbrMetallicRoughness": {"baseColorTexture": {"index": 0}},
76 "extensions": {"KHR_materials_specular": {"specularFactor": 0.5}}},
72 {"name": "YellowTimesFactor", "pbrMetallicRoughness": {"baseColorTexture": {"index": 1},
77 {"name": "YellowTimesFactor", "pbrMetallicRoughness": {"baseColorTexture": {"index": 1},
73 "baseColorFactor": [0.5, 1.0, 1.0, 1.0]}},
78 "baseColorFactor": [0.5, 1.0, 1.0, 1.0]},
79 "extensions": {"KHR_materials_specular": {"specularTexture": {"index": 2}}}},
74 ],
80 ],
75 "textures": [{"sampler": 0, "source": 0}, {"sampler": 0, "source": 1}],
81 "textures": [{"sampler": 0, "source": 0}, {"sampler": 0, "source": 1}, {"sampler": 0, "source": 2}],
76 "samplers": [{"magFilter": 9728, "minFilter": 9728}],
82 "samplers": [{"magFilter": 9728, "minFilter": 9728}],
77 "images": [{"uri": "textures/green_baseColor.png"}, {"uri": "textures/yellow_baseColor.png"}],
83 "images": [{"uri": "textures/green_baseColor.png"}, {"uri": "textures/yellow_baseColor.png"},
84 {"uri": "textures/specular.png"}],
78 "animations": [{"name": "idle", "samplers": [{"input": times, "output": rots}],
85 "animations": [{"name": "idle", "samplers": [{"input": times, "output": rots}],
79 "channels": [{"sampler": 0, "target": {"node": 0, "path": "rotation"}}]}],
86 "channels": [{"sampler": 0, "target": {"node": 0, "path": "rotation"}}]}],
80 "buffers": [{"uri": "body.bin", "byteLength": len(blob)}],
87 "buffers": [{"uri": "body.bin", "byteLength": len(blob)}],
src/fbx_scene.rs +2 −0
@@ -681,6 +681,8 @@ fn extract_mesh(
681 681 albedo: seed.albedo.clone(),
682 682 cutoff: seed.cutoff,
683 683 flat_normals: !mesh.vertex_normal.exists,
684 specular: 0.,
685 shiny: None,
684 686 });
685 687 }
686 688 }
@@ -681,6 +681,8 @@ fn extract_mesh(
681 albedo: seed.albedo.clone(),
681 albedo: seed.albedo.clone(),
682 cutoff: seed.cutoff,
682 cutoff: seed.cutoff,
683 flat_normals: !mesh.vertex_normal.exists,
683 flat_normals: !mesh.vertex_normal.exists,
684 specular: 0.,
685 shiny: None,
684 });
686 });
685 }
687 }
686 }
688 }
src/preview.rs +71 −34
@@ -155,6 +155,10 @@ pub struct BodySurface {
155 155 pub albedo: Option<std::sync::Arc<crate::equipment::Texture>>,
156 156 pub cutoff: f32,
157 157 pub flat_normals: bool,
158 /// Specular strength (KHR_materials_specular `specularFactor`; 0 without
159 /// the extension) and its strength texture (alpha copied into RGB).
160 pub specular: f32,
161 pub shiny: Option<std::sync::Arc<crate::equipment::Texture>>,
158 162 }
159 163
160 164 #[derive(Debug, Clone)]
@@ -462,6 +466,8 @@ impl StaticAcc {
462 466 albedo,
463 467 cutoff,
464 468 flat_normals,
469 specular: 0.,
470 shiny: None,
465 471 });
466 472 Ok(())
467 473 }
@@ -685,6 +691,8 @@ fn load_gltf_file_unguarded(path: &Path) -> Result<PreviewCharacter, String> {
685 691 color: prim.color,
686 692 albedo: prim.albedo,
687 693 cutoff: prim.cutoff,
694 specular: prim.specular,
695 shiny: prim.shiny,
688 696 });
689 697 }
690 698 }
@@ -799,6 +807,8 @@ struct GltfPrimitive {
799 807 color: [f32; 4],
800 808 albedo: Option<std::sync::Arc<crate::equipment::Texture>>,
801 809 cutoff: f32,
810 specular: f32,
811 shiny: Option<std::sync::Arc<crate::equipment::Texture>>,
802 812 flat_normals: bool,
803 813 }
804 814
@@ -871,41 +881,22 @@ fn read_gltf_primitive(
871 881 return Err("glTF-primitive heeft indices buiten het vertexbereik".into());
872 882 }
873 883 let albedo = texture
874 .map(|t| {
875 let image = images
876 .get(t.texture().source().index())
877 .ok_or("ontbrekende bodytexture")?;
878 use gltf::image::Format;
879 let channels = match image.format {
880 Format::R8 => 1,
881 Format::R8G8 => 2,
882 Format::R8G8B8 => 3,
883 Format::R8G8B8A8 => 4,
884 _ => return Err("bodytexture moet 8-bit zijn".to_string()),
885 };
886 if image.width > 2048 || image.height > 2048 {
887 return Err("bodytexture groter dan 2048×2048".into());
888 }
889 let mut rgba = Vec::new();
890 for p in image.pixels.chunks_exact(channels) {
891 rgba.extend_from_slice(&match channels {
892 1 => [p[0], p[0], p[0], 255],
893 2 => [p[0], p[0], p[0], p[1]],
894 3 => [p[0], p[1], p[2], 255],
895 _ => [p[0], p[1], p[2], p[3]],
896 });
897 }
898 use std::hash::{Hash, Hasher};
899 let mut hash = std::collections::hash_map::DefaultHasher::new();
900 rgba.hash(&mut hash);
901 Ok(std::sync::Arc::new(crate::equipment::Texture {
902 content_hash: hash.finish(),
903 width: image.width,
904 height: image.height,
905 rgba,
906 }))
907 })
884 .as_ref()
885 .map(|t| gltf_texture(images, t.texture().source().index(), false))
908 886 .transpose()?;
887 // KHR_materials_specular: strength = specularFactor × specularTexture.a,
888 // sampled with the base-colour UVs (other texCoord sets are ignored).
889 let (specular, shiny) = match material.specular() {
890 Some(ext) => {
891 let shiny = ext
892 .specular_texture()
893 .filter(|t| t.tex_coord() == uvset)
894 .map(|t| gltf_texture(images, t.texture().source().index(), true))
895 .transpose()?;
896 (ext.specular_factor().clamp(0., 1.), shiny)
897 }
898 None => (0., None),
899 };
909 900 Ok(GltfPrimitive {
910 901 flat_normals: normals.is_none(),
911 902 positions,
@@ -922,9 +913,55 @@ fn read_gltf_primitive(
922 913 } else {
923 914 0.001
924 915 },
916 specular,
917 shiny,
925 918 })
926 919 }
927 920
921 /// One 8-bit glTF image as RGBA8; `alpha_to_rgb` copies the alpha channel into
922 /// RGB (strength textures such as KHR_materials_specular keep it in A).
923 fn gltf_texture(
924 images: &[gltf::image::Data],
925 index: usize,
926 alpha_to_rgb: bool,
927 ) -> Result<std::sync::Arc<crate::equipment::Texture>, String> {
928 let image = images.get(index).ok_or("ontbrekende bodytexture")?;
929 use gltf::image::Format;
930 let channels = match image.format {
931 Format::R8 => 1,
932 Format::R8G8 => 2,
933 Format::R8G8B8 => 3,
934 Format::R8G8B8A8 => 4,
935 _ => return Err("bodytexture moet 8-bit zijn".to_string()),
936 };
937 if image.width > 2048 || image.height > 2048 {
938 return Err("bodytexture groter dan 2048×2048".into());
939 }
940 let mut rgba = Vec::new();
941 for p in image.pixels.chunks_exact(channels) {
942 let px = match channels {
943 1 => [p[0], p[0], p[0], 255],
944 2 => [p[0], p[0], p[0], p[1]],
945 3 => [p[0], p[1], p[2], 255],
946 _ => [p[0], p[1], p[2], p[3]],
947 };
948 rgba.extend_from_slice(&if alpha_to_rgb {
949 [px[3], px[3], px[3], 255]
950 } else {
951 px
952 });
953 }
954 use std::hash::{Hash, Hasher};
955 let mut hash = std::collections::hash_map::DefaultHasher::new();
956 rgba.hash(&mut hash);
957 Ok(std::sync::Arc::new(crate::equipment::Texture {
958 content_hash: hash.finish(),
959 width: image.width,
960 height: image.height,
961 rgba,
962 }))
963 }
964
928 965 /// glTF without a skin: every mesh instance of the default scene, with its
929 966 /// node's rest world transform baked into positions (and the inverse-
930 967 /// transpose into normals) exactly once; mirrored transforms flip winding.
@@ -155,6 +155,10 @@ pub struct BodySurface {
155 pub albedo: Option<std::sync::Arc<crate::equipment::Texture>>,
155 pub albedo: Option<std::sync::Arc<crate::equipment::Texture>>,
156 pub cutoff: f32,
156 pub cutoff: f32,
157 pub flat_normals: bool,
157 pub flat_normals: bool,
158 /// Specular strength (KHR_materials_specular `specularFactor`; 0 without
159 /// the extension) and its strength texture (alpha copied into RGB).
160 pub specular: f32,
161 pub shiny: Option<std::sync::Arc<crate::equipment::Texture>>,
158}
162}
159
163
160#[derive(Debug, Clone)]
164#[derive(Debug, Clone)]
@@ -462,6 +466,8 @@ impl StaticAcc {
462 albedo,
466 albedo,
463 cutoff,
467 cutoff,
464 flat_normals,
468 flat_normals,
469 specular: 0.,
470 shiny: None,
465 });
471 });
466 Ok(())
472 Ok(())
467 }
473 }
@@ -685,6 +691,8 @@ fn load_gltf_file_unguarded(path: &Path) -> Result<PreviewCharacter, String> {
685 color: prim.color,
691 color: prim.color,
686 albedo: prim.albedo,
692 albedo: prim.albedo,
687 cutoff: prim.cutoff,
693 cutoff: prim.cutoff,
694 specular: prim.specular,
695 shiny: prim.shiny,
688 });
696 });
689 }
697 }
690 }
698 }
@@ -799,6 +807,8 @@ struct GltfPrimitive {
799 color: [f32; 4],
807 color: [f32; 4],
800 albedo: Option<std::sync::Arc<crate::equipment::Texture>>,
808 albedo: Option<std::sync::Arc<crate::equipment::Texture>>,
801 cutoff: f32,
809 cutoff: f32,
810 specular: f32,
811 shiny: Option<std::sync::Arc<crate::equipment::Texture>>,
802 flat_normals: bool,
812 flat_normals: bool,
803}
813}
804
814
@@ -871,41 +881,22 @@ fn read_gltf_primitive(
871 return Err("glTF-primitive heeft indices buiten het vertexbereik".into());
881 return Err("glTF-primitive heeft indices buiten het vertexbereik".into());
872 }
882 }
873 let albedo = texture
883 let albedo = texture
874 .map(|t| {
884 .as_ref()
875 let image = images
885 .map(|t| gltf_texture(images, t.texture().source().index(), false))
876 .get(t.texture().source().index())
877 .ok_or("ontbrekende bodytexture")?;
878 use gltf::image::Format;
879 let channels = match image.format {
880 Format::R8 => 1,
881 Format::R8G8 => 2,
882 Format::R8G8B8 => 3,
883 Format::R8G8B8A8 => 4,
884 _ => return Err("bodytexture moet 8-bit zijn".to_string()),
885 };
886 if image.width > 2048 || image.height > 2048 {
887 return Err("bodytexture groter dan 2048×2048".into());
888 }
889 let mut rgba = Vec::new();
890 for p in image.pixels.chunks_exact(channels) {
891 rgba.extend_from_slice(&match channels {
892 1 => [p[0], p[0], p[0], 255],
893 2 => [p[0], p[0], p[0], p[1]],
894 3 => [p[0], p[1], p[2], 255],
895 _ => [p[0], p[1], p[2], p[3]],
896 });
897 }
898 use std::hash::{Hash, Hasher};
899 let mut hash = std::collections::hash_map::DefaultHasher::new();
900 rgba.hash(&mut hash);
901 Ok(std::sync::Arc::new(crate::equipment::Texture {
902 content_hash: hash.finish(),
903 width: image.width,
904 height: image.height,
905 rgba,
906 }))
907 })
908 .transpose()?;
886 .transpose()?;
887 // KHR_materials_specular: strength = specularFactor × specularTexture.a,
888 // sampled with the base-colour UVs (other texCoord sets are ignored).
889 let (specular, shiny) = match material.specular() {
890 Some(ext) => {
891 let shiny = ext
892 .specular_texture()
893 .filter(|t| t.tex_coord() == uvset)
894 .map(|t| gltf_texture(images, t.texture().source().index(), true))
895 .transpose()?;
896 (ext.specular_factor().clamp(0., 1.), shiny)
897 }
898 None => (0., None),
899 };
909 Ok(GltfPrimitive {
900 Ok(GltfPrimitive {
910 flat_normals: normals.is_none(),
901 flat_normals: normals.is_none(),
911 positions,
902 positions,
@@ -922,9 +913,55 @@ fn read_gltf_primitive(
922 } else {
913 } else {
923 0.001
914 0.001
924 },
915 },
916 specular,
917 shiny,
925 })
918 })
926}
919}
927
920
921/// One 8-bit glTF image as RGBA8; `alpha_to_rgb` copies the alpha channel into
922/// RGB (strength textures such as KHR_materials_specular keep it in A).
923fn gltf_texture(
924 images: &[gltf::image::Data],
925 index: usize,
926 alpha_to_rgb: bool,
927) -> Result<std::sync::Arc<crate::equipment::Texture>, String> {
928 let image = images.get(index).ok_or("ontbrekende bodytexture")?;
929 use gltf::image::Format;
930 let channels = match image.format {
931 Format::R8 => 1,
932 Format::R8G8 => 2,
933 Format::R8G8B8 => 3,
934 Format::R8G8B8A8 => 4,
935 _ => return Err("bodytexture moet 8-bit zijn".to_string()),
936 };
937 if image.width > 2048 || image.height > 2048 {
938 return Err("bodytexture groter dan 2048×2048".into());
939 }
940 let mut rgba = Vec::new();
941 for p in image.pixels.chunks_exact(channels) {
942 let px = match channels {
943 1 => [p[0], p[0], p[0], 255],
944 2 => [p[0], p[0], p[0], p[1]],
945 3 => [p[0], p[1], p[2], 255],
946 _ => [p[0], p[1], p[2], p[3]],
947 };
948 rgba.extend_from_slice(&if alpha_to_rgb {
949 [px[3], px[3], px[3], 255]
950 } else {
951 px
952 });
953 }
954 use std::hash::{Hash, Hasher};
955 let mut hash = std::collections::hash_map::DefaultHasher::new();
956 rgba.hash(&mut hash);
957 Ok(std::sync::Arc::new(crate::equipment::Texture {
958 content_hash: hash.finish(),
959 width: image.width,
960 height: image.height,
961 rgba,
962 }))
963}
964
928/// glTF without a skin: every mesh instance of the default scene, with its
965/// glTF without a skin: every mesh instance of the default scene, with its
929/// node's rest world transform baked into positions (and the inverse-
966/// node's rest world transform baked into positions (and the inverse-
930/// transpose into normals) exactly once; mirrored transforms flip winding.
967/// transpose into normals) exactly once; mirrored transforms flip winding.
src/preview_gl.rs +43 −1
@@ -317,6 +317,8 @@ fn body_frame(
317 317 color: s.color,
318 318 albedo: s.albedo.clone(),
319 319 cutoff: s.cutoff,
320 specular: s.specular,
321 shiny: s.shiny.clone(),
320 322 })
321 323 .collect();
322 324 PendingFrame {
@@ -363,6 +365,9 @@ fn character_generation(character: &PreviewCharacter) -> u64 {
363 365 if let Some(t) = &s.albedo {
364 366 (t.content_hash, t.width, t.height).hash(&mut hash);
365 367 }
368 if let Some(t) = &s.shiny {
369 (1u8, t.content_hash, t.width, t.height).hash(&mut hash);
370 }
366 371 }
367 372 hash.finish()
368 373 }
@@ -400,6 +405,8 @@ struct SurfaceDraw {
400 405 color: [f32; 4],
401 406 albedo: Option<std::sync::Arc<crate::equipment::Texture>>,
402 407 cutoff: f32,
408 specular: f32,
409 shiny: Option<std::sync::Arc<crate::equipment::Texture>>,
403 410 }
404 411
405 412 impl PendingFrame {
@@ -466,6 +473,8 @@ struct GpuCache {
466 473 textures: [Option<glow::Texture>; 2],
467 474 /// One albedo texture per body surface (same order as `surfaces`).
468 475 surface_textures: Vec<Option<glow::Texture>>,
476 /// One specular strength texture per body surface.
477 surface_shiny: Vec<Option<glow::Texture>>,
469 478 layers: Vec<GpuCache>,
470 479 texture_cache: crate::equipment::TextureCache,
471 480 }
@@ -569,6 +578,8 @@ impl GpuCache {
569 578 gl.draw_elements(glow::TRIANGLES, self.index_count, glow::UNSIGNED_INT, 0);
570 579 } else {
571 580 let albedo_enabled = gl.get_uniform_location(program, "u_albedo_enabled");
581 let shiny_enabled = gl.get_uniform_location(program, "u_shiny_enabled");
582 let specular = gl.get_uniform_location(program, "u_specular");
572 583 for (index, surface) in frame.surfaces.iter().enumerate() {
573 584 let end = (surface.first + surface.count).min(self.index_count as usize);
574 585 if surface.first >= end {
@@ -578,6 +589,12 @@ impl GpuCache {
578 589 gl.active_texture(glow::TEXTURE0);
579 590 gl.bind_texture(glow::TEXTURE_2D, texture);
580 591 gl.uniform_1_i32(albedo_enabled.as_ref(), i32::from(texture.is_some()));
592 let shiny = self.surface_shiny.get(index).copied().flatten();
593 gl.active_texture(glow::TEXTURE1);
594 gl.bind_texture(glow::TEXTURE_2D, shiny);
595 gl.active_texture(glow::TEXTURE0);
596 gl.uniform_1_i32(shiny_enabled.as_ref(), i32::from(shiny.is_some()));
597 gl.uniform_1_f32(specular.as_ref(), surface.specular);
581 598 gl.uniform_4_f32(
582 599 color.as_ref(),
583 600 surface.color[0],
@@ -628,6 +645,7 @@ impl GpuCache {
628 645 }
629 646 }
630 647 self.surface_textures.clear();
648 self.surface_shiny.clear();
631 649 if let Some(v) = self.vao.take() {
632 650 gl.delete_vertex_array(v)
633 651 }
@@ -706,7 +724,12 @@ impl GpuCache {
706 724 self.textures[unit] = Some(upload_texture(gl, texture)?);
707 725 }
708 726 }
709 for old in self.surface_textures.drain(..).flatten() {
727 for old in self
728 .surface_textures
729 .drain(..)
730 .chain(self.surface_shiny.drain(..))
731 .flatten()
732 {
710 733 gl.delete_texture(old);
711 734 }
712 735 gl.active_texture(glow::TEXTURE0);
@@ -716,6 +739,11 @@ impl GpuCache {
716 739 None => None,
717 740 };
718 741 self.surface_textures.push(id);
742 let shiny = match &surface.shiny {
743 Some(texture) => Some(upload_texture(gl, texture)?),
744 None => None,
745 };
746 self.surface_shiny.push(shiny);
719 747 }
720 748 self.uploaded = frame.generation;
721 749 self.index_count = frame.indices.len() as i32;
@@ -1076,6 +1104,8 @@ pub unsafe fn depth_regression_probe(gl: &glow::Context) -> Result<(), String> {
1076 1104 color: [1., 0., 0., 1.],
1077 1105 albedo: None,
1078 1106 cutoff: 0.001,
1107 specular: 0.,
1108 shiny: None,
1079 1109 },
1080 1110 SurfaceDraw {
1081 1111 first: 3,
@@ -1083,6 +1113,8 @@ pub unsafe fn depth_regression_probe(gl: &glow::Context) -> Result<(), String> {
1083 1113 color: [1., 0.5, 1., 1.],
1084 1114 albedo: Some(cyan.clone()),
1085 1115 cutoff: 0.001,
1116 specular: 0.,
1117 shiny: None,
1086 1118 },
1087 1119 ];
1088 1120 cache.pending = Some(body.clone());
@@ -1159,6 +1191,16 @@ mod tests {
1159 1191 &frame.surfaces[2].albedo.as_ref().unwrap().rgba[..4],
1160 1192 [200, 200, 0, 255]
1161 1193 );
1194 // KHR_materials_specular (Tim t998u.4): factor and alpha texture per material.
1195 assert_eq!(frame.surfaces[0].specular, 0.);
1196 assert!(frame.surfaces[0].shiny.is_none());
1197 assert_eq!(frame.surfaces[1].specular, 0.5);
1198 assert!(frame.surfaces[1].shiny.is_none());
1199 assert_eq!(frame.surfaces[2].specular, 1.0);
1200 assert_eq!(
1201 &frame.surfaces[2].shiny.as_ref().unwrap().rgba[..4],
1202 [102, 102, 102, 255]
1203 );
1162 1204 // A different texture must re-upload: it is part of the generation.
1163 1205 let mut changed = body.clone();
1164 1206 std::sync::Arc::make_mut(changed.surfaces[1].albedo.as_mut().unwrap()).content_hash ^= 1;
@@ -317,6 +317,8 @@ fn body_frame(
317 color: s.color,
317 color: s.color,
318 albedo: s.albedo.clone(),
318 albedo: s.albedo.clone(),
319 cutoff: s.cutoff,
319 cutoff: s.cutoff,
320 specular: s.specular,
321 shiny: s.shiny.clone(),
320 })
322 })
321 .collect();
323 .collect();
322 PendingFrame {
324 PendingFrame {
@@ -363,6 +365,9 @@ fn character_generation(character: &PreviewCharacter) -> u64 {
363 if let Some(t) = &s.albedo {
365 if let Some(t) = &s.albedo {
364 (t.content_hash, t.width, t.height).hash(&mut hash);
366 (t.content_hash, t.width, t.height).hash(&mut hash);
365 }
367 }
368 if let Some(t) = &s.shiny {
369 (1u8, t.content_hash, t.width, t.height).hash(&mut hash);
370 }
366 }
371 }
367 hash.finish()
372 hash.finish()
368}
373}
@@ -400,6 +405,8 @@ struct SurfaceDraw {
400 color: [f32; 4],
405 color: [f32; 4],
401 albedo: Option<std::sync::Arc<crate::equipment::Texture>>,
406 albedo: Option<std::sync::Arc<crate::equipment::Texture>>,
402 cutoff: f32,
407 cutoff: f32,
408 specular: f32,
409 shiny: Option<std::sync::Arc<crate::equipment::Texture>>,
403}
410}
404
411
405impl PendingFrame {
412impl PendingFrame {
@@ -466,6 +473,8 @@ struct GpuCache {
466 textures: [Option<glow::Texture>; 2],
473 textures: [Option<glow::Texture>; 2],
467 /// One albedo texture per body surface (same order as `surfaces`).
474 /// One albedo texture per body surface (same order as `surfaces`).
468 surface_textures: Vec<Option<glow::Texture>>,
475 surface_textures: Vec<Option<glow::Texture>>,
476 /// One specular strength texture per body surface.
477 surface_shiny: Vec<Option<glow::Texture>>,
469 layers: Vec<GpuCache>,
478 layers: Vec<GpuCache>,
470 texture_cache: crate::equipment::TextureCache,
479 texture_cache: crate::equipment::TextureCache,
471}
480}
@@ -569,6 +578,8 @@ impl GpuCache {
569 gl.draw_elements(glow::TRIANGLES, self.index_count, glow::UNSIGNED_INT, 0);
578 gl.draw_elements(glow::TRIANGLES, self.index_count, glow::UNSIGNED_INT, 0);
570 } else {
579 } else {
571 let albedo_enabled = gl.get_uniform_location(program, "u_albedo_enabled");
580 let albedo_enabled = gl.get_uniform_location(program, "u_albedo_enabled");
581 let shiny_enabled = gl.get_uniform_location(program, "u_shiny_enabled");
582 let specular = gl.get_uniform_location(program, "u_specular");
572 for (index, surface) in frame.surfaces.iter().enumerate() {
583 for (index, surface) in frame.surfaces.iter().enumerate() {
573 let end = (surface.first + surface.count).min(self.index_count as usize);
584 let end = (surface.first + surface.count).min(self.index_count as usize);
574 if surface.first >= end {
585 if surface.first >= end {
@@ -578,6 +589,12 @@ impl GpuCache {
578 gl.active_texture(glow::TEXTURE0);
589 gl.active_texture(glow::TEXTURE0);
579 gl.bind_texture(glow::TEXTURE_2D, texture);
590 gl.bind_texture(glow::TEXTURE_2D, texture);
580 gl.uniform_1_i32(albedo_enabled.as_ref(), i32::from(texture.is_some()));
591 gl.uniform_1_i32(albedo_enabled.as_ref(), i32::from(texture.is_some()));
592 let shiny = self.surface_shiny.get(index).copied().flatten();
593 gl.active_texture(glow::TEXTURE1);
594 gl.bind_texture(glow::TEXTURE_2D, shiny);
595 gl.active_texture(glow::TEXTURE0);
596 gl.uniform_1_i32(shiny_enabled.as_ref(), i32::from(shiny.is_some()));
597 gl.uniform_1_f32(specular.as_ref(), surface.specular);
581 gl.uniform_4_f32(
598 gl.uniform_4_f32(
582 color.as_ref(),
599 color.as_ref(),
583 surface.color[0],
600 surface.color[0],
@@ -628,6 +645,7 @@ impl GpuCache {
628 }
645 }
629 }
646 }
630 self.surface_textures.clear();
647 self.surface_textures.clear();
648 self.surface_shiny.clear();
631 if let Some(v) = self.vao.take() {
649 if let Some(v) = self.vao.take() {
632 gl.delete_vertex_array(v)
650 gl.delete_vertex_array(v)
633 }
651 }
@@ -706,7 +724,12 @@ impl GpuCache {
706 self.textures[unit] = Some(upload_texture(gl, texture)?);
724 self.textures[unit] = Some(upload_texture(gl, texture)?);
707 }
725 }
708 }
726 }
709 for old in self.surface_textures.drain(..).flatten() {
727 for old in self
728 .surface_textures
729 .drain(..)
730 .chain(self.surface_shiny.drain(..))
731 .flatten()
732 {
710 gl.delete_texture(old);
733 gl.delete_texture(old);
711 }
734 }
712 gl.active_texture(glow::TEXTURE0);
735 gl.active_texture(glow::TEXTURE0);
@@ -716,6 +739,11 @@ impl GpuCache {
716 None => None,
739 None => None,
717 };
740 };
718 self.surface_textures.push(id);
741 self.surface_textures.push(id);
742 let shiny = match &surface.shiny {
743 Some(texture) => Some(upload_texture(gl, texture)?),
744 None => None,
745 };
746 self.surface_shiny.push(shiny);
719 }
747 }
720 self.uploaded = frame.generation;
748 self.uploaded = frame.generation;
721 self.index_count = frame.indices.len() as i32;
749 self.index_count = frame.indices.len() as i32;
@@ -1076,6 +1104,8 @@ pub unsafe fn depth_regression_probe(gl: &glow::Context) -> Result<(), String> {
1076 color: [1., 0., 0., 1.],
1104 color: [1., 0., 0., 1.],
1077 albedo: None,
1105 albedo: None,
1078 cutoff: 0.001,
1106 cutoff: 0.001,
1107 specular: 0.,
1108 shiny: None,
1079 },
1109 },
1080 SurfaceDraw {
1110 SurfaceDraw {
1081 first: 3,
1111 first: 3,
@@ -1083,6 +1113,8 @@ pub unsafe fn depth_regression_probe(gl: &glow::Context) -> Result<(), String> {
1083 color: [1., 0.5, 1., 1.],
1113 color: [1., 0.5, 1., 1.],
1084 albedo: Some(cyan.clone()),
1114 albedo: Some(cyan.clone()),
1085 cutoff: 0.001,
1115 cutoff: 0.001,
1116 specular: 0.,
1117 shiny: None,
1086 },
1118 },
1087 ];
1119 ];
1088 cache.pending = Some(body.clone());
1120 cache.pending = Some(body.clone());
@@ -1159,6 +1191,16 @@ mod tests {
1159 &frame.surfaces[2].albedo.as_ref().unwrap().rgba[..4],
1191 &frame.surfaces[2].albedo.as_ref().unwrap().rgba[..4],
1160 [200, 200, 0, 255]
1192 [200, 200, 0, 255]
1161 );
1193 );
1194 // KHR_materials_specular (Tim t998u.4): factor and alpha texture per material.
1195 assert_eq!(frame.surfaces[0].specular, 0.);
1196 assert!(frame.surfaces[0].shiny.is_none());
1197 assert_eq!(frame.surfaces[1].specular, 0.5);
1198 assert!(frame.surfaces[1].shiny.is_none());
1199 assert_eq!(frame.surfaces[2].specular, 1.0);
1200 assert_eq!(
1201 &frame.surfaces[2].shiny.as_ref().unwrap().rgba[..4],
1202 [102, 102, 102, 255]
1203 );
1162 // A different texture must re-upload: it is part of the generation.
1204 // A different texture must re-upload: it is part of the generation.
1163 let mut changed = body.clone();
1205 let mut changed = body.clone();
1164 std::sync::Arc::make_mut(changed.surfaces[1].albedo.as_mut().unwrap()).content_hash ^= 1;
1206 std::sync::Arc::make_mut(changed.surfaces[1].albedo.as_mut().unwrap()).content_hash ^= 1;
src/production.rs +2 −2
@@ -364,8 +364,8 @@ pub fn scene(
364 364 indices: body.indices[surface.first..surface.first + surface.count].to_vec(),
365 365 color: surface.color,
366 366 albedo: surface.albedo.clone(),
367 shiny: None,
368 specular: 0.,
367 shiny: surface.shiny.clone(),
368 specular: surface.specular,
369 369 cutoff: surface.cutoff,
370 370 flat_normals: surface.flat_normals,
371 371 });
@@ -364,8 +364,8 @@ pub fn scene(
364 indices: body.indices[surface.first..surface.first + surface.count].to_vec(),
364 indices: body.indices[surface.first..surface.first + surface.count].to_vec(),
365 color: surface.color,
365 color: surface.color,
366 albedo: surface.albedo.clone(),
366 albedo: surface.albedo.clone(),
367 shiny: None,
367 shiny: surface.shiny.clone(),
368 specular: 0.,
368 specular: surface.specular,
369 cutoff: surface.cutoff,
369 cutoff: surface.cutoff,
370 flat_normals: surface.flat_normals,
370 flat_normals: surface.flat_normals,
371 });
371 });
tests/fixtures/bodies/multi-material/body.gltf +22 −0
@@ -1,4 +1,7 @@
1 1 {
2 "extensionsUsed": [
3 "KHR_materials_specular"
4 ],
2 5 "asset": {
3 6 "version": "2.0",
4 7 "generator": "paperdoll generate_multi_material_body.py (CC0)"
@@ -91,6 +94,11 @@
91 94 "baseColorTexture": {
92 95 "index": 0
93 96 }
97 },
98 "extensions": {
99 "KHR_materials_specular": {
100 "specularFactor": 0.5
101 }
94 102 }
95 103 },
96 104 {
@@ -105,6 +113,13 @@
105 113 1.0,
106 114 1.0
107 115 ]
116 },
117 "extensions": {
118 "KHR_materials_specular": {
119 "specularTexture": {
120 "index": 2
121 }
122 }
108 123 }
109 124 }
110 125 ],
@@ -116,6 +131,10 @@
116 131 {
117 132 "sampler": 0,
118 133 "source": 1
134 },
135 {
136 "sampler": 0,
137 "source": 2
119 138 }
120 139 ],
121 140 "samplers": [
@@ -130,6 +149,9 @@
130 149 },
131 150 {
132 151 "uri": "textures/yellow_baseColor.png"
152 },
153 {
154 "uri": "textures/specular.png"
133 155 }
134 156 ],
135 157 "animations": [
@@ -1,4 +1,7 @@
1{
1{
2 "extensionsUsed": [
3 "KHR_materials_specular"
4 ],
2 "asset": {
5 "asset": {
3 "version": "2.0",
6 "version": "2.0",
4 "generator": "paperdoll generate_multi_material_body.py (CC0)"
7 "generator": "paperdoll generate_multi_material_body.py (CC0)"
@@ -91,6 +94,11 @@
91 "baseColorTexture": {
94 "baseColorTexture": {
92 "index": 0
95 "index": 0
93 }
96 }
97 },
98 "extensions": {
99 "KHR_materials_specular": {
100 "specularFactor": 0.5
101 }
94 }
102 }
95 },
103 },
96 {
104 {
@@ -105,6 +113,13 @@
105 1.0,
113 1.0,
106 1.0
114 1.0
107 ]
115 ]
116 },
117 "extensions": {
118 "KHR_materials_specular": {
119 "specularTexture": {
120 "index": 2
121 }
122 }
108 }
123 }
109 }
124 }
110 ],
125 ],
@@ -116,6 +131,10 @@
116 {
131 {
117 "sampler": 0,
132 "sampler": 0,
118 "source": 1
133 "source": 1
134 },
135 {
136 "sampler": 0,
137 "source": 2
119 }
138 }
120 ],
139 ],
121 "samplers": [
140 "samplers": [
@@ -130,6 +149,9 @@
130 },
149 },
131 {
150 {
132 "uri": "textures/yellow_baseColor.png"
151 "uri": "textures/yellow_baseColor.png"
152 },
153 {
154 "uri": "textures/specular.png"
133 }
155 }
134 ],
156 ],
135 "animations": [
157 "animations": [
tests/fixtures/bodies/multi-material/textures/specular.png added Binary file +0 −0

The contents of this binary file are not shown.