BlueGit

golden-age paperdoll-renderer

Files

paperdoll-renderer / src / equipment.rs

equipment.rs

equipment.rs 27.0 KiB Download Raw
1//! Rigid equipment, portable embedded assets and a bounded binary container.
2//! Local rotation uses Rz * Ry * Rx (degrees); attachment uses the animated
3//! joint world transform, never a skin matrix or inverse bind a second time.
4
5use std::collections::BTreeSet;
6use std::io::{Cursor, Read, Write};
7use std::path::Path;
8
9use crate::preview::{
10 animated_joint_world, identity, joint_paths, mul, Mat4, PreviewCharacter, PreviewControls,
11};
12use crate::project::portable_relative;
13use serde::{Deserialize, Serialize};
14
15pub const MAGIC: &[u8; 8] = b"PPEQ\r\n\x1a\n";
16pub const VERSION: u32 = 1;
17pub const MAX_FILE: usize = 32 * 1024 * 1024;
18const MAX_META: usize = 4 * 1024 * 1024;
19const MAX_ASSET: usize = 8 * 1024 * 1024;
20const MAX_ASSETS: usize = 32;
21const MAX_VERTICES: usize = 80_000;
22
23#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
24#[serde(deny_unknown_fields)]
25pub struct EmbeddedAsset {
26 pub path: String,
27 #[serde(skip)]
28 pub bytes: Vec<u8>,
29}
30
31#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
32#[serde(deny_unknown_fields)]
33pub struct Mesh {
34 pub positions: Vec<[f32; 3]>,
35 pub normals: Vec<[f32; 3]>,
36 pub uvs: Vec<[f32; 2]>,
37 pub indices: Vec<u32>,
38 pub color: [f32; 4],
39 pub albedo: Option<usize>,
40}
41
42#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
43#[serde(deny_unknown_fields)]
44pub struct RigSignature {
45 pub paths: Vec<String>,
46 pub rest_world: Vec<Mat4>,
47}
48impl RigSignature {
49 pub fn of(body: &PreviewCharacter) -> Self {
50 Self {
51 paths: joint_paths(body),
52 rest_world: (0..body.joint_nodes.len())
53 .map(|j| animated_joint_world(body, "", 0.0, j).unwrap_or_else(identity))
54 .collect(),
55 }
56 }
57}
58
59#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
60#[serde(deny_unknown_fields)]
61pub struct Attachment {
62 pub label: String,
63 pub bones: [Option<String>; 2],
64 pub translation: [f32; 3],
65 pub rotation_deg: [f32; 3],
66 pub meshes: Vec<Mesh>,
67 pub shiny: Option<usize>,
68 pub specular: f32,
69}
70
71#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
72#[serde(deny_unknown_fields)]
73pub struct Preset {
74 pub name: String,
75 pub provenance: String,
76 pub applies: [bool; 2],
77 pub rigs: [Option<RigSignature>; 2],
78 pub attachments: Vec<Attachment>,
79 pub assets: Vec<EmbeddedAsset>,
80}
81impl Preset {
82 pub fn new(name: String) -> Self {
83 Self {
84 name,
85 provenance: String::new(),
86 applies: [true, true],
87 rigs: [None, None],
88 attachments: Vec::new(),
89 assets: Vec::new(),
90 }
91 }
92 pub fn bind(
93 &mut self,
94 sex: usize,
95 body: &PreviewCharacter,
96 attachment: usize,
97 joint: usize,
98 ) -> Result<(), String> {
99 if sex > 1 {
100 return Err("ongeldig geslacht".into());
101 }
102 let paths = joint_paths(body);
103 let path = paths.get(joint).ok_or("dit bot bestaat niet")?.clone();
104 let item = self
105 .attachments
106 .get_mut(attachment)
107 .ok_or("dit onderdeel bestaat niet")?;
108 item.bones[sex] = Some(path);
109 self.rigs[sex] = Some(RigSignature::of(body));
110 Ok(())
111 }
112 pub fn validate(&self) -> Result<(), String> {
113 if !valid_name(&self.name) {
114 return Err(
115 "equipmentnaam: kleine letters/cijfers/-/_, geen spaties, en niet 'body'".into(),
116 );
117 }
118 if self.provenance.len() > 4096 || self.provenance.contains('\0') {
119 return Err("herkomst/licentie is te lang of bevat NUL".into());
120 }
121 if !self.applies.iter().any(|x| *x) {
122 return Err("kies minstens één geslacht".into());
123 }
124 if self.attachments.len() > 32 || self.assets.len() > MAX_ASSETS {
125 return Err("te veel onderdelen of bestanden".into());
126 }
127 let mut paths = BTreeSet::new();
128 let mut total = 0usize;
129 for asset in &self.assets {
130 portable_relative(&asset.path)?;
131 if asset.path.len() > 240 || asset.bytes.len() > MAX_ASSET || !paths.insert(&asset.path)
132 {
133 return Err("ongeldige, dubbele of te grote asset".into());
134 }
135 total = total
136 .checked_add(asset.bytes.len())
137 .ok_or("te veel assetdata")?;
138 }
139 if total > MAX_FILE {
140 return Err("te veel assetdata".into());
141 }
142 let mut texture_indices = BTreeSet::new();
143 let mut vertices = 0;
144 for rig in self.rigs.iter().flatten() {
145 if rig.paths.len() > 128
146 || rig.paths.len() != rig.rest_world.len()
147 || rig.paths.iter().any(|p| p.len() > 4096)
148 || rig.rest_world.iter().flatten().any(|x| !x.is_finite())
149 {
150 return Err("ongeldige rigbinding".into());
151 }
152 }
153 for attachment in &self.attachments {
154 for sex in 0..2 {
155 if let Some(path) = &attachment.bones[sex] {
156 if !self.rigs[sex]
157 .as_ref()
158 .map(|rig| rig.paths.contains(path))
159 .unwrap_or(false)
160 {
161 return Err("attachment verwijst naar onbekend bot".into());
162 }
163 }
164 }
165 if attachment.label.len() > 128
166 || attachment.bones.iter().flatten().any(|p| p.len() > 4096)
167 || attachment
168 .translation
169 .iter()
170 .chain(attachment.rotation_deg.iter())
171 .any(|x| !x.is_finite() || x.abs() > 100_000.0)
172 || !attachment.specular.is_finite()
173 || !(0.0..=1.0).contains(&attachment.specular)
174 || attachment.meshes.len() > 64
175 {
176 return Err("ongeldige attachment/transformatie".into());
177 }
178 if let Some(index) = attachment.shiny {
179 texture_indices.insert(index);
180 }
181 for mesh in &attachment.meshes {
182 vertices += mesh.positions.len();
183 if vertices > MAX_VERTICES
184 || mesh.indices.len() > MAX_VERTICES * 6
185 || mesh.indices.len() < 3
186 || mesh.indices.len() % 3 != 0
187 || mesh.positions.len() != mesh.normals.len()
188 || mesh.positions.len() != mesh.uvs.len()
189 || mesh.positions.is_empty()
190 || mesh
191 .indices
192 .iter()
193 .any(|i| *i as usize >= mesh.positions.len())
194 || mesh
195 .positions
196 .iter()
197 .flatten()
198 .chain(mesh.normals.iter().flatten())
199 .chain(mesh.uvs.iter().flatten())
200 .chain(mesh.color.iter())
201 .any(|x| !x.is_finite() || x.abs() > 1_000_000.0)
202 {
203 return Err("ongeldige of te grote equipmentmesh".into());
204 }
205 if let Some(index) = mesh.albedo {
206 texture_indices.insert(index);
207 }
208 }
209 }
210 let mut decoded = 0usize;
211 for index in texture_indices {
212 decoded += self.texture(index)?.rgba.len();
213 if decoded > 64 * 1024 * 1024 {
214 return Err("textures samen groter dan 64 MiB".into());
215 }
216 }
217 Ok(())
218 }
219 pub fn texture(&self, index: usize) -> Result<Texture, String> {
220 let asset = self
221 .assets
222 .get(index)
223 .ok_or("textureverwijzing ontbreekt")?;
224 decode_png(&asset.bytes)
225 }
226 pub fn encode(&self) -> Result<Vec<u8>, String> {
227 self.validate()?;
228 let metadata = serde_json::to_vec(self).map_err(|e| e.to_string())?;
229 if metadata.len() > MAX_META {
230 return Err("presetmetadata is te groot".into());
231 }
232 let mut payload = metadata;
233 let meta_len = payload.len();
234 for asset in &self.assets {
235 payload.extend_from_slice(&(asset.bytes.len() as u32).to_le_bytes());
236 payload.extend_from_slice(&asset.bytes);
237 }
238 if payload.len() + 20 > MAX_FILE {
239 return Err("preset is te groot".into());
240 }
241 let mut out = MAGIC.to_vec();
242 out.extend_from_slice(&VERSION.to_le_bytes());
243 out.extend_from_slice(&(meta_len as u32).to_le_bytes());
244 out.extend_from_slice(&checksum(&payload).to_le_bytes());
245 out.extend(payload);
246 Ok(out)
247 }
248 pub fn decode(bytes: &[u8]) -> Result<Self, String> {
249 if bytes.len() < 20 || bytes.len() > MAX_FILE || &bytes[..8] != MAGIC {
250 return Err("ongeldig equipmentbestand of bestand te groot".into());
251 }
252 let number = |offset| u32::from_le_bytes(bytes[offset..offset + 4].try_into().unwrap());
253 if number(8) != VERSION {
254 return Err("onbekende equipmentversie".into());
255 }
256 let meta_len = number(12) as usize;
257 if meta_len > MAX_META
258 || meta_len > bytes.len() - 20
259 || checksum(&bytes[20..]) != number(16)
260 {
261 return Err("beschadigde equipmentdata".into());
262 }
263 let mut preset: Self =
264 serde_json::from_slice(&bytes[20..20 + meta_len]).map_err(|e| e.to_string())?;
265 if preset.assets.len() > MAX_ASSETS {
266 return Err("te veel assets".into());
267 }
268 let mut cursor = 20 + meta_len;
269 for asset in &mut preset.assets {
270 if cursor + 4 > bytes.len() {
271 return Err("afgebroken asset".into());
272 }
273 let len = number(cursor) as usize;
274 cursor += 4;
275 if len > MAX_ASSET || len > bytes.len() - cursor {
276 return Err("ongeldige assetlengte".into());
277 }
278 asset.bytes = bytes[cursor..cursor + len].to_vec();
279 cursor += len;
280 }
281 if cursor != bytes.len() {
282 return Err("onverwachte extra equipmentdata".into());
283 }
284 preset.validate()?;
285 Ok(preset)
286 }
287 pub fn read(path: &Path) -> Result<Self, String> {
288 let file = std::fs::File::open(path).map_err(|e| e.to_string())?;
289 let mut bytes = Vec::new();
290 file.take((MAX_FILE + 1) as u64)
291 .read_to_end(&mut bytes)
292 .map_err(|e| e.to_string())?;
293 Self::decode(&bytes)
294 }
295}
296
297pub fn valid_name(name: &str) -> bool {
298 !name.is_empty()
299 && name.len() <= 64
300 && name != "body"
301 && name
302 .bytes()
303 .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == b'-' || c == b'_')
304}
305
306pub fn checksum(bytes: &[u8]) -> u32 {
307 let mut crc = !0u32;
308 for byte in bytes {
309 crc ^= u32::from(*byte);
310 for _ in 0..8 {
311 crc = (crc >> 1) ^ (0xedb88320u32 & (0u32.wrapping_sub(crc & 1)));
312 }
313 }
314 !crc
315}
316
317/// Immutable snapshot publish: a hard link atomically creates the destination
318/// without overwriting an older saved preset. Failure leaves old data intact.
319pub fn write_snapshot(path: &Path, bytes: &[u8]) -> Result<(), String> {
320 if bytes.len() > MAX_FILE {
321 return Err("snapshot te groot".into());
322 }
323 if path.exists() {
324 let mut current = Vec::new();
325 std::fs::File::open(path)
326 .map_err(|e| e.to_string())?
327 .take((MAX_FILE + 1) as u64)
328 .read_to_end(&mut current)
329 .map_err(|e| e.to_string())?;
330 return if current == bytes {
331 Ok(())
332 } else {
333 Err("snapshotnaam bestaat met andere inhoud".into())
334 };
335 }
336 let stamp = std::time::SystemTime::now()
337 .duration_since(std::time::UNIX_EPOCH)
338 .unwrap_or_default()
339 .as_nanos();
340 let temp = path.with_extension(format!("ppe.part-{}-{stamp}", std::process::id()));
341 let result = (|| {
342 let mut file = std::fs::OpenOptions::new()
343 .write(true)
344 .create_new(true)
345 .open(&temp)
346 .map_err(|e| e.to_string())?;
347 file.write_all(bytes)
348 .and_then(|_| file.sync_all())
349 .map_err(|e| e.to_string())?;
350 std::fs::hard_link(&temp, path).map_err(|e| e.to_string())?;
351 Ok(())
352 })();
353 let _ = std::fs::remove_file(temp);
354 result
355}
356
357#[derive(Debug, Clone, PartialEq)]
358pub struct Texture {
359 pub content_hash: u64,
360 pub width: u32,
361 pub height: u32,
362 pub rgba: Vec<u8>,
363}
364fn decode_png(bytes: &[u8]) -> Result<Texture, String> {
365 let mut decoder = png::Decoder::new(Cursor::new(bytes));
366 decoder.set_transformations(png::Transformations::EXPAND | png::Transformations::STRIP_16);
367 let mut reader = decoder.read_info().map_err(|e| e.to_string())?;
368 let info = reader.info();
369 if info.width == 0 || info.height == 0 || info.width > 2048 || info.height > 2048 {
370 return Err("texture is groter dan 2048×2048".into());
371 }
372 let size = reader
373 .output_buffer_size()
374 .ok_or("ongeldige texturegrootte")?;
375 if size > 2048 * 2048 * 8 {
376 return Err("texture is te groot".into());
377 }
378 let mut data = vec![0; size];
379 let info = reader.next_frame(&mut data).map_err(|e| e.to_string())?;
380 let channels = info.color_type.samples();
381 let mut rgba = Vec::with_capacity(info.width as usize * info.height as usize * 4);
382 for pixel in data[..info.buffer_size()].chunks_exact(channels) {
383 match info.color_type {
384 png::ColorType::Grayscale => {
385 rgba.extend_from_slice(&[pixel[0], pixel[0], pixel[0], 255])
386 }
387 png::ColorType::GrayscaleAlpha => {
388 rgba.extend_from_slice(&[pixel[0], pixel[0], pixel[0], pixel[1]])
389 }
390 png::ColorType::Rgb => rgba.extend_from_slice(&[pixel[0], pixel[1], pixel[2], 255]),
391 png::ColorType::Rgba => rgba.extend_from_slice(pixel),
392 _ => return Err("onbekende texturecodering".into()),
393 }
394 }
395 use std::hash::{Hash, Hasher};
396 let mut hash = std::collections::hash_map::DefaultHasher::new();
397 rgba.hash(&mut hash);
398 Ok(Texture {
399 content_hash: hash.finish(),
400 width: info.width,
401 height: info.height,
402 rgba,
403 })
404}
405
406pub fn local_matrix(translation: [f32; 3], degrees: [f32; 3]) -> Mat4 {
407 let [x, y, z] = degrees.map(f32::to_radians);
408 let (sx, cx) = x.sin_cos();
409 let (sy, cy) = y.sin_cos();
410 let (sz, cz) = z.sin_cos();
411 let rx = [
412 1., 0., 0., 0., 0., cx, sx, 0., 0., -sx, cx, 0., 0., 0., 0., 1.,
413 ];
414 let ry = [
415 cy, 0., -sy, 0., 0., 1., 0., 0., sy, 0., cy, 0., 0., 0., 0., 1.,
416 ];
417 let rz = [
418 cz, sz, 0., 0., -sz, cz, 0., 0., 0., 0., 1., 0., 0., 0., 0., 1.,
419 ];
420 let mut matrix = mul(rz, mul(ry, rx));
421 matrix[12] = translation[0];
422 matrix[13] = translation[1];
423 matrix[14] = translation[2];
424 matrix
425}
426
427#[derive(Debug, Clone)]
428pub struct Layer {
429 pub mesh: Mesh,
430 pub model: Mat4,
431 pub albedo: Option<std::sync::Arc<Texture>>,
432 pub shiny: Option<std::sync::Arc<Texture>>,
433 pub specular: f32,
434}
435
436#[derive(Default)]
437pub struct TextureCache {
438 entries: std::collections::VecDeque<(Vec<u8>, std::sync::Arc<Texture>)>,
439 bytes: usize,
440}
441impl TextureCache {
442 fn get(&mut self, preset: &Preset, index: usize) -> Result<std::sync::Arc<Texture>, String> {
443 let bytes = &preset.assets.get(index).ok_or("texture ontbreekt")?.bytes;
444 if let Some((_, texture)) = self.entries.iter().find(|(source, _)| source == bytes) {
445 return Ok(texture.clone());
446 }
447 let texture = std::sync::Arc::new(decode_png(bytes)?);
448 let cost = bytes.len() + texture.rgba.len();
449 while self.bytes + cost > 128 * 1024 * 1024 {
450 if let Some((source, image)) = self.entries.pop_front() {
451 self.bytes -= source.len() + image.rgba.len();
452 } else {
453 break;
454 }
455 }
456 self.bytes += cost;
457 self.entries.push_back((bytes.clone(), texture.clone()));
458 Ok(texture)
459 }
460}
461
462pub fn layers(
463 preset: &Preset,
464 sex: usize,
465 body: &PreviewCharacter,
466 controls: &PreviewControls,
467) -> Result<Vec<Layer>, String> {
468 layers_cached(preset, sex, body, controls, &mut TextureCache::default())
469}
470
471pub fn layers_cached(
472 preset: &Preset,
473 sex: usize,
474 body: &PreviewCharacter,
475 controls: &PreviewControls,
476 cache: &mut TextureCache,
477) -> Result<Vec<Layer>, String> {
478 if sex > 1 {
479 return Err("ongeldig geslacht".into());
480 }
481 if !preset.applies[sex] {
482 return Ok(Vec::new());
483 }
484 let signature = RigSignature::of(body);
485 if preset.rigs[sex].as_ref() != Some(&signature) {
486 return Err("rig wijkt af: kies de botten opnieuw voor dit lichaam".into());
487 }
488 let paths = joint_paths(body);
489 let mut layers = Vec::new();
490 for item in &preset.attachments {
491 let path = item.bones[sex]
492 .as_deref()
493 .ok_or("kies een bot voor dit onderdeel en geslacht")?;
494 let joint = paths
495 .iter()
496 .position(|p| p == path)
497 .ok_or("het gekoppelde bot ontbreekt")?;
498 let world = animated_joint_world(body, &controls.clip_name, controls.time, joint)
499 .ok_or("het gekoppelde bot ontbreekt")?;
500 let model = mul(world, local_matrix(item.translation, item.rotation_deg));
501 let shiny = item.shiny.map(|i| cache.get(preset, i)).transpose()?;
502 for mesh in &item.meshes {
503 layers.push(Layer {
504 mesh: mesh.clone(),
505 model,
506 albedo: mesh.albedo.map(|i| cache.get(preset, i)).transpose()?,
507 shiny: shiny.clone(),
508 specular: item.specular,
509 });
510 }
511 }
512 Ok(layers)
513}
514
515/// Import an OBJ and its declared MTL/PNG dependencies into the preset itself.
516/// No extraction directories, absolute references or parent traversal allowed.
517pub fn import_obj(preset: &mut Preset, source: &Path) -> Result<usize, String> {
518 if source
519 .extension()
520 .and_then(|x| x.to_str())
521 .map(|x| !x.eq_ignore_ascii_case("obj"))
522 .unwrap_or(true)
523 {
524 return Err("kies een rigide OBJ-model met MTL/PNG-bijbestanden".into());
525 }
526 let root = source
527 .parent()
528 .ok_or("modelmap ontbreekt")?
529 .canonicalize()
530 .map_err(|e| e.to_string())?;
531 let name = source
532 .file_name()
533 .and_then(|x| x.to_str())
534 .ok_or("ongeldige modelnaam")?;
535 let mut candidate = preset.clone();
536 if candidate.attachments.len() >= 32 {
537 return Err("te veel onderdelen".into());
538 }
539 let prefix = format!("part-{}", candidate.attachments.len());
540 let obj_bytes = read_asset(&root, name)?;
541 let obj_text = std::str::from_utf8(&obj_bytes).map_err(|e| e.to_string())?;
542 let mut dependencies = BTreeSet::new();
543 dependencies.insert(name.to_string());
544 for line in obj_text.lines() {
545 if let Some(rest) = line.trim().strip_prefix("mtllib ") {
546 let mtl = rest.trim();
547 dependencies.insert(mtl.to_string());
548 let bytes = read_asset(&root, mtl)?;
549 let text = std::str::from_utf8(&bytes).map_err(|e| e.to_string())?;
550 for line in text.lines() {
551 let line = line.trim();
552 if line.starts_with("map_")
553 || line.starts_with("bump ")
554 || line.starts_with("disp ")
555 {
556 let (_, path) = line
557 .split_once(char::is_whitespace)
558 .ok_or("ongeldige textureverwijzing")?;
559 let relative = Path::new(mtl)
560 .parent()
561 .unwrap_or(Path::new(""))
562 .join(path.trim());
563 let relative = relative
564 .to_str()
565 .ok_or("ongeldig texturepad")?
566 .replace('\\', "/");
567 portable_relative(&relative)?;
568 dependencies.insert(relative);
569 }
570 }
571 }
572 }
573 if dependencies.len() + candidate.assets.len() > MAX_ASSETS {
574 return Err("te veel bijbestanden".into());
575 }
576 let mut asset_map = std::collections::BTreeMap::new();
577 let mut total_bytes: usize = candidate.assets.iter().map(|a| a.bytes.len()).sum();
578 for relative in dependencies {
579 let bytes = read_asset(&root, &relative)?;
580 total_bytes += bytes.len();
581 if total_bytes > MAX_FILE {
582 return Err("assets samen te groot".into());
583 }
584 let index = candidate.assets.len();
585 candidate.assets.push(EmbeddedAsset {
586 path: format!("{prefix}/{relative}"),
587 bytes,
588 });
589 asset_map.insert(relative, index);
590 }
591 // A checked callback prevents the parser from reading undeclared/unsafe files.
592 let mut input = std::io::BufReader::new(Cursor::new(obj_bytes));
593 let options = tobj::LoadOptions {
594 triangulate: true,
595 single_index: true,
596 ..Default::default()
597 };
598 let texture_paths = std::cell::RefCell::new(std::collections::BTreeMap::new());
599 let (models, materials) = tobj::load_obj_buf(&mut input, &options, |path| {
600 let relative = path.to_string_lossy().replace('\\', "/");
601 let index = asset_map
602 .get(&relative)
603 .ok_or(tobj::LoadError::OpenFileFailed)?;
604 let mut reader = std::io::BufReader::new(Cursor::new(&candidate.assets[*index].bytes));
605 let (materials, map) = tobj::load_mtl_buf(&mut reader)?;
606 for material in &materials {
607 if let Some(texture) = &material.diffuse_texture {
608 let relative = path
609 .parent()
610 .unwrap_or(Path::new(""))
611 .join(texture)
612 .to_string_lossy()
613 .replace('\\', "/");
614 texture_paths
615 .borrow_mut()
616 .insert(material.name.clone(), relative);
617 }
618 }
619 Ok((materials, map))
620 })
621 .map_err(|e| e.to_string())?;
622 let materials = materials.map_err(|e| e.to_string())?;
623 let texture_paths = texture_paths.into_inner();
624 let mut meshes = Vec::new();
625 let mut vertices: usize = candidate
626 .attachments
627 .iter()
628 .flat_map(|a| &a.meshes)
629 .map(|m| m.positions.len())
630 .sum();
631 for model in models {
632 let source = model.mesh;
633 vertices += source.positions.len() / 3;
634 if vertices > MAX_VERTICES || source.indices.len() > MAX_VERTICES * 6 {
635 return Err("equipmentmesh is te groot".into());
636 }
637 let positions: Vec<[f32; 3]> = source
638 .positions
639 .chunks_exact(3)
640 .map(|x| [x[0], x[1], x[2]])
641 .collect();
642 let mut normals: Vec<[f32; 3]> = source
643 .normals
644 .chunks_exact(3)
645 .map(|x| [x[0], x[1], x[2]])
646 .collect();
647 if normals.len() != positions.len() {
648 normals = compute_normals(&positions, &source.indices)?;
649 }
650 let mut uvs: Vec<[f32; 2]> = source
651 .texcoords
652 .chunks_exact(2)
653 .map(|x| [x[0], x[1]])
654 .collect();
655 if uvs.len() != positions.len() {
656 uvs = vec![[0.; 2]; positions.len()];
657 }
658 let material = source.material_id.and_then(|id| materials.get(id));
659 let color = material.and_then(|m| m.diffuse).unwrap_or([0.7, 0.7, 0.7]);
660 let albedo = material
661 .and_then(|m| texture_paths.get(&m.name))
662 .map(|path| {
663 asset_map
664 .get(path)
665 .copied()
666 .ok_or("ontbrekende diffuse texture")
667 })
668 .transpose()?;
669 meshes.push(Mesh {
670 positions,
671 normals,
672 uvs,
673 indices: source.indices,
674 color: [
675 color[0],
676 color[1],
677 color[2],
678 material.and_then(|m| m.dissolve).unwrap_or(1.),
679 ],
680 albedo,
681 });
682 }
683 if meshes.is_empty() {
684 return Err("het OBJ-bestand heeft geen meshes".into());
685 }
686 candidate.attachments.push(Attachment {
687 label: name.to_string(),
688 bones: [None, None],
689 translation: [0.; 3],
690 rotation_deg: [0.; 3],
691 meshes,
692 shiny: None,
693 specular: 0.25,
694 });
695 candidate.validate()?;
696 *preset = candidate;
697 Ok(preset.attachments.len() - 1)
698}
699
700pub fn import_shiny(preset: &mut Preset, attachment: usize, source: &Path) -> Result<(), String> {
701 let root = source.parent().ok_or("texturemap ontbreekt")?;
702 let name = source
703 .file_name()
704 .and_then(|x| x.to_str())
705 .ok_or("ongeldige bestandsnaam")?;
706 let bytes = read_asset(root, name)?;
707 decode_png(&bytes)?;
708 let mut candidate = preset.clone();
709 let index = candidate.assets.len();
710 candidate.assets.push(EmbeddedAsset {
711 path: format!("shiny-{index}/{name}"),
712 bytes,
713 });
714 candidate
715 .attachments
716 .get_mut(attachment)
717 .ok_or("onderdeel ontbreekt")?
718 .shiny = Some(index);
719 candidate.validate()?;
720 *preset = candidate;
721 Ok(())
722}
723
724fn read_asset(root: &Path, relative: &str) -> Result<Vec<u8>, String> {
725 portable_relative(relative)?;
726 let root = root.canonicalize().map_err(|e| e.to_string())?;
727 let path = root
728 .join(relative)
729 .canonicalize()
730 .map_err(|e| e.to_string())?;
731 if !path.starts_with(&root) {
732 return Err("asset verlaat de modelmap".into());
733 }
734 let file = std::fs::File::open(path).map_err(|e| e.to_string())?;
735 let mut bytes = Vec::new();
736 file.take((MAX_ASSET + 1) as u64)
737 .read_to_end(&mut bytes)
738 .map_err(|e| e.to_string())?;
739 if bytes.len() > MAX_ASSET {
740 return Err("asset is te groot".into());
741 }
742 Ok(bytes)
743}
744fn compute_normals(positions: &[[f32; 3]], indices: &[u32]) -> Result<Vec<[f32; 3]>, String> {
745 let mut normals = vec![[0.; 3]; positions.len()];
746 for t in indices.chunks_exact(3) {
747 let [a, b, c] = [t[0] as usize, t[1] as usize, t[2] as usize];
748 if [a, b, c].iter().any(|i| *i >= positions.len()) {
749 return Err("ongeldige OBJ-index".into());
750 }
751 let ab = std::array::from_fn::<_, 3, _>(|i| positions[b][i] - positions[a][i]);
752 let ac = std::array::from_fn::<_, 3, _>(|i| positions[c][i] - positions[a][i]);
753 let n = [
754 ab[1] * ac[2] - ab[2] * ac[1],
755 ab[2] * ac[0] - ab[0] * ac[2],
756 ab[0] * ac[1] - ab[1] * ac[0],
757 ];
758 for vertex in [a, b, c] {
759 for (i, value) in n.iter().enumerate() {
760 normals[vertex][i] += value;
761 }
762 }
763 }
764 for normal in &mut normals {
765 let length = normal.iter().map(|v| v * v).sum::<f32>().sqrt();
766 if length > 1e-10 {
767 for v in normal {
768 *v /= length
769 }
770 }
771 }
772 Ok(normals)
773}