1use std::io::Write;
8use std::path::Path;
9
10use bytemuck::cast_slice;
11use oxihuman_mesh::MeshBuffers;
12use serde_json::json;
13
14use crate::material::PbrMaterial;
15use crate::texture::PixelBuffer;
16
17const GLB_MAGIC: u32 = 0x46546C67; const GLB_VERSION: u32 = 2;
20const CHUNK_JSON: u32 = 0x4E4F534A; const CHUNK_BIN: u32 = 0x004E4942; pub struct EmbeddedTexture {
25 pub name: String,
26 pub pixels: Vec<u8>,
28 pub width: u32,
29 pub height: u32,
30 pub mime_type: String,
32}
33
34impl EmbeddedTexture {
35 pub fn from_pixel_buffer(name: impl Into<String>, buf: &PixelBuffer) -> Self {
37 Self {
38 name: name.into(),
39 pixels: buf.pixels.clone(),
40 width: buf.width,
41 height: buf.height,
42 mime_type: "image/x-raw-rgba".into(),
43 }
44 }
45
46 pub fn from_tga(name: impl Into<String>, buf: &PixelBuffer) -> Self {
48 Self {
49 name: name.into(),
50 pixels: buf.to_tga_bytes(),
51 width: buf.width,
52 height: buf.height,
53 mime_type: "image/x-tga".into(),
54 }
55 }
56
57 pub fn byte_len(&self) -> usize {
59 self.pixels.len()
60 }
61}
62
63pub fn export_glb_with_texture(
72 mesh: &MeshBuffers,
73 material: &PbrMaterial,
74 texture: &EmbeddedTexture,
75 path: &Path,
76) -> anyhow::Result<()> {
77 let n_verts = mesh.positions.len();
79 let n_idx = mesh.indices.len();
80
81 let pos_bytes: &[u8] = cast_slice(&mesh.positions);
82 let norm_bytes: &[u8] = cast_slice(&mesh.normals);
83 let uv_bytes: &[u8] = cast_slice(&mesh.uvs);
84 let idx_bytes: &[u8] = cast_slice(&mesh.indices);
85
86 let pos_offset = 0usize;
87 let norm_offset = pos_offset + pos_bytes.len();
88 let uv_offset = norm_offset + norm_bytes.len();
89 let idx_offset = uv_offset + uv_bytes.len();
90 let mesh_bin_len = idx_offset + idx_bytes.len();
91
92 let tex_offset = mesh_bin_len;
94
95 let mut bin_data: Vec<u8> = Vec::with_capacity(mesh_bin_len + texture.byte_len() + 3);
96 bin_data.extend_from_slice(pos_bytes);
97 bin_data.extend_from_slice(norm_bytes);
98 bin_data.extend_from_slice(uv_bytes);
99 bin_data.extend_from_slice(idx_bytes);
100 bin_data.extend_from_slice(&texture.pixels);
101
102 while !bin_data.len().is_multiple_of(4) {
104 bin_data.push(0x00);
105 }
106 let total_bin = bin_data.len() as u32;
107
108 let accessors: Vec<serde_json::Value> = vec![
112 json!({ "bufferView": 0, "componentType": 5126, "count": n_verts, "type": "VEC3" }),
113 json!({ "bufferView": 1, "componentType": 5126, "count": n_verts, "type": "VEC3" }),
114 json!({ "bufferView": 2, "componentType": 5126, "count": n_verts, "type": "VEC2" }),
115 json!({ "bufferView": 3, "componentType": 5125, "count": n_idx, "type": "SCALAR" }),
116 ];
117
118 let buffer_views: Vec<serde_json::Value> = vec![
119 json!({ "buffer": 0, "byteOffset": pos_offset, "byteLength": pos_bytes.len() }),
120 json!({ "buffer": 0, "byteOffset": norm_offset, "byteLength": norm_bytes.len() }),
121 json!({ "buffer": 0, "byteOffset": uv_offset, "byteLength": uv_bytes.len() }),
122 json!({ "buffer": 0, "byteOffset": idx_offset, "byteLength": idx_bytes.len() }),
123 json!({ "buffer": 0, "byteOffset": tex_offset, "byteLength": texture.byte_len() }),
125 ];
126
127 let gltf = json!({
128 "asset": { "version": "2.0", "generator": "OxiHuman 0.1.0" },
129 "scene": 0,
130 "scenes": [{ "nodes": [0] }],
131 "nodes": [{ "mesh": 0 }],
132 "meshes": [{
133 "name": "human",
134 "primitives": [{
135 "attributes": {
136 "POSITION": 0,
137 "NORMAL": 1,
138 "TEXCOORD_0": 2
139 },
140 "indices": 3,
141 "mode": 4,
142 "material": 0
143 }]
144 }],
145 "materials": [{
146 "name": material.name,
147 "pbrMetallicRoughness": {
148 "baseColorFactor": [
149 material.base_color[0],
150 material.base_color[1],
151 material.base_color[2],
152 material.base_color[3]
153 ],
154 "baseColorTexture": { "index": 0 },
155 "metallicFactor": material.metallic,
156 "roughnessFactor": material.roughness
157 },
158 "emissiveFactor": [
159 material.emissive[0],
160 material.emissive[1],
161 material.emissive[2]
162 ],
163 "doubleSided": material.double_sided,
164 "alphaMode": material.alpha_mode
165 }],
166 "textures": [{ "source": 0, "sampler": 0 }],
167 "images": [{
168 "name": texture.name,
169 "bufferView": 4,
170 "mimeType": texture.mime_type
171 }],
172 "samplers": [{
173 "magFilter": 9729,
174 "minFilter": 9987,
175 "wrapS": 10497,
176 "wrapT": 10497
177 }],
178 "accessors": accessors,
179 "bufferViews": buffer_views,
180 "buffers": [{ "byteLength": total_bin }]
181 });
182
183 let mut json_bytes = serde_json::to_vec(&gltf)?;
184 while json_bytes.len() % 4 != 0 {
186 json_bytes.push(b' ');
187 }
188
189 let json_chunk_len = json_bytes.len() as u32;
191 let bin_chunk_len = bin_data.len() as u32;
192 let total_len = 12 + 8 + json_chunk_len + 8 + bin_chunk_len;
193
194 let mut file = std::fs::File::create(path)?;
195
196 file.write_all(&GLB_MAGIC.to_le_bytes())?;
198 file.write_all(&GLB_VERSION.to_le_bytes())?;
199 file.write_all(&total_len.to_le_bytes())?;
200
201 file.write_all(&json_chunk_len.to_le_bytes())?;
203 file.write_all(&CHUNK_JSON.to_le_bytes())?;
204 file.write_all(&json_bytes)?;
205
206 file.write_all(&bin_chunk_len.to_le_bytes())?;
208 file.write_all(&CHUNK_BIN.to_le_bytes())?;
209 file.write_all(&bin_data)?;
210
211 Ok(())
212}
213
214#[cfg(test)]
215mod tests {
216 use super::*;
217 use crate::glb::export_glb;
218 use crate::material::PbrMaterial;
219 use crate::texture::generate_skin_texture;
220
221 fn tri_mesh() -> MeshBuffers {
222 MeshBuffers {
223 positions: vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]],
224 normals: vec![[0.0, 0.0, 1.0]; 3],
225 uvs: vec![[0.0, 0.0]; 3],
226 tangents: vec![],
227 colors: None,
228 indices: vec![0, 1, 2],
229 has_suit: true,
230 }
231 }
232
233 fn small_tex() -> EmbeddedTexture {
234 let buf = generate_skin_texture(4, 4, 200, 160, 140);
235 EmbeddedTexture::from_tga("skin", &buf)
236 }
237
238 #[test]
239 fn embedded_texture_byte_len_positive() {
240 assert!(small_tex().byte_len() > 0);
241 }
242
243 #[test]
244 fn from_pixel_buffer_dimensions() {
245 let buf = generate_skin_texture(8, 6, 180, 140, 120);
246 let tex = EmbeddedTexture::from_pixel_buffer("t", &buf);
247 assert_eq!(tex.width, buf.width);
248 assert_eq!(tex.height, buf.height);
249 }
250
251 #[test]
252 fn export_glb_with_texture_creates_file() {
253 let mesh = tri_mesh();
254 let mat = PbrMaterial::skin();
255 let tex = small_tex();
256 let path = std::path::PathBuf::from("/tmp/test_tex_embed_create.glb");
257 export_glb_with_texture(&mesh, &mat, &tex, &path).expect("export failed");
258 assert!(path.exists(), "GLB file was not created");
259 std::fs::remove_file(&path).ok();
260 }
261
262 #[test]
263 fn glb_with_texture_valid_header() {
264 let mesh = tri_mesh();
265 let mat = PbrMaterial::skin();
266 let tex = small_tex();
267 let path = std::path::PathBuf::from("/tmp/test_tex_embed_header.glb");
268 export_glb_with_texture(&mesh, &mat, &tex, &path).expect("export failed");
269 let bytes = std::fs::read(&path).expect("should succeed");
270 assert!(bytes.len() >= 4, "file too short to contain GLB magic");
271 let magic = u32::from_le_bytes(bytes[0..4].try_into().expect("should succeed"));
272 assert_eq!(magic, 0x46546C67u32, "first 4 bytes are not GLB magic");
273 std::fs::remove_file(&path).ok();
274 }
275
276 #[test]
277 fn glb_with_texture_larger_than_without() {
278 let mesh = tri_mesh();
279 let mat = PbrMaterial::skin();
280 let tex = small_tex();
281
282 let path_plain = std::path::PathBuf::from("/tmp/test_tex_embed_plain.glb");
283 let path_tex = std::path::PathBuf::from("/tmp/test_tex_embed_with_tex.glb");
284
285 export_glb(&mesh, &path_plain).expect("plain export failed");
287 export_glb_with_texture(&mesh, &mat, &tex, &path_tex).expect("texture export failed");
288
289 let size_plain = std::fs::metadata(&path_plain).expect("should succeed").len();
290 let size_tex = std::fs::metadata(&path_tex).expect("should succeed").len();
291
292 assert!(
293 size_tex > size_plain,
294 "GLB with texture ({} bytes) should be larger than plain ({} bytes)",
295 size_tex,
296 size_plain
297 );
298
299 std::fs::remove_file(&path_plain).ok();
300 std::fs::remove_file(&path_tex).ok();
301 }
302}