glTF KHR_materials_specular → body specular map (factor × texture alpha), live highlight per primitive; fixture + checks (Tim t998u.4)
@@ -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 gl10
eframe = { version = "0.29.1", default-features = false, features = ["default_fonts", "glow", "x11", "wayland"] }10
eframe = { version = "0.29.1", default-features = false, features = ["default_fonts", "glow", "x11", "wayland"] }11
egui_glow = "0.29.1"11
egui_glow = "0.29.1"12
glow = "0.14.2"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
libloading = "0.8"14
libloading = "0.8"15
png = "0.18"15
png = "0.18"16
rfd = "0.15.4"16
rfd = "0.15.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** selects36
36
37
### Multiple meshes and skins (glTF/GLB)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
### 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 `.p115
- `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.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
```text120
```text121
renders/male-body-atlas/raw files/male/body/clip001-walk/f000012/p-60_y330_diffuse.png121
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 /tmp221
cargo run --release --example resume_fixture --locked -- verify /tmp/resume221
cargo run --release --example resume_fixture --locked -- verify /tmp/resume222
# FBX on a real GPU: live view (body + FBX sword) and full production of body and equipment set222
# FBX on a real GPU: live view (body + FBX sword) and full production of body and equipment set223
xvfb-run -a cargo run --example fbx_live_fixture --locked -- /tmp/fbx-live /tmp/fbx-live-proof 32 48 4223
xvfb-run -a cargo run --example fbx_live_fixture --locked -- /tmp/fbx-live /tmp/fbx-live-proof 32 48 4224
# 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 diffuse225
# screenshots the live view and checks every material colour in the production diffuse and the specular strengths226
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-material226
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-material227
# Depth range check on the CPU: per pitch the depth extent of every clip frame and view versus near/far227
# Depth range check on the CPU: per pitch the depth extent of every clip frame and view versus near/far228
cargo run --release --example depth_clamp_probe --locked -- tests/fixtures/bodies/test-body-male.glb 2600 1000 4228
cargo run --release --example depth_clamp_probe --locked -- tests/fixtures/bodies/test-body-male.glb 2600 1000 4@@ -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> = spec230
.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
}@@ -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 python31
#!/usr/bin/env python32
"""CC0 regression fixture: a skinned glTF body with an external .bin, two2
"""CC0 regression fixture: a skinned glTF body with an external .bin, three3
external PNG textures and three materials (factor only, texture, texture x3
external PNG textures and three materials (factor only, texture, texture x4
factor), one primitive per material. Three quads side by side facing +Z.4
factor), one primitive per material. Three quads side by side facing +Z.5
KHR_materials_specular: Green has specularFactor 0.5; YellowTimesFactor has6
specularFactor 1 with an RGBA specular texture whose alpha is 102 (0.4).5
7
6
Writes tests/fixtures/bodies/multi-material/{body.gltf,body.bin,textures/*.png}.8
Writes tests/fixtures/bodies/multi-material/{body.gltf,body.bin,textures/*.png}.7
"""9
"""@@ -10,11 +12,11 @@ import json, os, struct, zlib10
OUT = os.path.join(os.path.dirname(__file__), "..", "tests", "fixtures", "bodies", "multi-material")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
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)}],@@ -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
}@@ -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 without159
/// 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 = texture883
let albedo = texture874
.map(|t| {884
.as_ref()875
let image = images885
.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 = ext892
.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.001914
0.001924
},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 into922
/// 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
px952
});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 its965
/// glTF without a skin: every mesh instance of the default scene, with its929
/// 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.@@ -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
405
impl PendingFrame {412
impl 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 self728
.surface_textures729
.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;@@ -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
});@@ -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": 095
"index": 093
}96
}97
},98
"extensions": {99
"KHR_materials_specular": {100
"specularFactor": 0.5101
}94
}102
}95
},103
},96
{104
{@@ -105,6 +113,13 @@105
1.0,113
1.0,106
1.0114
1.0107
]115
]116
},117
"extensions": {118
"KHR_materials_specular": {119
"specularTexture": {120
"index": 2121
}122
}108
}123
}109
}124
}110
],125
],@@ -116,6 +131,10 @@116
{131
{117
"sampler": 0,132
"sampler": 0,118
"source": 1133
"source": 1134
},135
{136
"sampler": 0,137
"source": 2119
}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": [The contents of this binary file are not shown.