Framing: labelled orientation convention test + one-shot all-animation envelope (Omhullende animaties)
@@ -21,9 +21,9 @@ De eerste projectweergave toont de bodies. **Kies man / Kies vrouw** selecteert
21
21
22
22
**Open man / Open vrouw** opent de body. Een geskind glTF/GLB- of FBX-model, of een statisch lichaam (zie hieronder), is in de live preview zichtbaar:
23
23
24
- Sleep om te orbiten. **Vast renderkader tonen** toont het gele orthografische exportkader; stel per man/vrouw **Vaste renderhoogte**, **Centrum Y** (beeldcentrum) en **Pivot Y** (orbitpunt) in en sla op. Zonder handmatige pivot gebruikt de editor de rustpose-nekhoogte (of een fallback met diagnose); **Auto nek** wist een handmatige override. Body en equipment delen exact dezelfde camera. Uitvinken geeft vrije perspectief-preview met scrollzoom; die zoom verandert de export niet. Een echte 24-bits depthbuffer houdt voorste oppervlakken vooraan.
24
- Sleep om te orbiten. **Vast renderkader tonen** toont het gele orthografische exportkader; stel per man/vrouw **Vaste renderhoogte**, **Centrum Y** (beeldcentrum) en **Pivot Y** (orbitpunt) in en sla op. Zonder handmatige pivot gebruikt de editor de rustpose-nekhoogte (of een fallback met diagnose); **Auto nek** wist een handmatige override. **Omhullende animaties** berekent eenmalig een renderhoogte en Centrum Y waarbinnen alle clips, alle frames (op project-fps) en alle 60 hoeken vallen, met 5% marge aan elke kant; de pivot blijft staan. Daarna is het kader weer vast: er is geen automatische zoom per frame, clip of laag. Equipment dat verder uitsteekt dan het lichaam vraagt handmatig extra hoogte. Body en equipment delen exact dezelfde camera. Uitvinken geeft vrije perspectief-preview met scrollzoom; die zoom verandert de export niet. Een echte 24-bits depthbuffer houdt voorste oppervlakken vooraan.
25
25
- Kies een animatieclip en gebruik **Pauze / Speel**.
26
- **Yaw − / Yaw +** volgen twaalf hoeken van 30°; **pitch -60 / -30 / 0 / 30 / 60** kiezen de hoogte. Yaw 0 kijkt naar de voorkant van de testlichamen (−Z).
26
- **Yaw − / Yaw +** volgen twaalf hoeken van 30°; **pitch -60 / -30 / 0 / 30 / 60** kiezen de hoogte. Yaw 0 kijkt naar de voorkant van de testlichamen (−Z). Vaste oriëntatie (+Y omhoog, het lichaam kijkt naar −Z, zijn rechterzijde is +X): yaw 0 = voorkant (zijn rechterhand links in beeld), 90 = linkerzijde, 180 = achterkant, 270 = rechterzijde; positieve pitch kijkt van boven. Een test met gelabelde markers bewaakt deze afspraak.
27
27
- **Render alle hoeken** rendert alle animaties × alle frames op project-fps × 60 hoeken (5 pitches × 12 yaws). Na de losse maps schrijft hij per maptype een atlas plus een `.ppda`-bestand (zie *Atlassen en .ppda*). De preview wordt direct vervangen door iedere voltooide pass: highres → detail → target/diffuse → depth → normal → specular. Alle passes behouden hun aspect met gecentreerde letterboxing en nearest-neighbor weergave. Voortgang telt echte passes; **Annuleer render** behoudt geldige gedeeltelijke uitvoer. **Live 3D** keert terug naar het model; een volgende voltooide renderpass kan de preview weer vervangen.
28
28
- Stel breedte, hoogte en fps in de Bodies-weergave in. Nieuwe projecten beginnen op 4 fps; een bestaand `.ppd` houdt zijn opgeslagen fps.
29
29
- **Zoek onderbroken runs** (ook in de equipment-editor) toont geannuleerde, mislukte of afgebroken runs van dezelfde body/equipmentset met het aantal vastgelegde PNG's. **Hervat** gaat alleen verder als bronbestanden en instellingen exact gelijk zijn aan die run; anders staat erbij wat er verschilt. Na annuleren verschijnt de run direct in deze lijst. Zie *Hervatten* hieronder.
@@ -21,9 +21,9 @@ De eerste projectweergave toont de bodies. **Kies man / Kies vrouw** selecteert21
21
22
**Open man / Open vrouw** opent de body. Een geskind glTF/GLB- of FBX-model, of een statisch lichaam (zie hieronder), is in de live preview zichtbaar:22
**Open man / Open vrouw** opent de body. Een geskind glTF/GLB- of FBX-model, of een statisch lichaam (zie hieronder), is in de live preview zichtbaar:23
23
24
- Sleep om te orbiten. **Vast renderkader tonen** toont het gele orthografische exportkader; stel per man/vrouw **Vaste renderhoogte**, **Centrum Y** (beeldcentrum) en **Pivot Y** (orbitpunt) in en sla op. Zonder handmatige pivot gebruikt de editor de rustpose-nekhoogte (of een fallback met diagnose); **Auto nek** wist een handmatige override. Body en equipment delen exact dezelfde camera. Uitvinken geeft vrije perspectief-preview met scrollzoom; die zoom verandert de export niet. Een echte 24-bits depthbuffer houdt voorste oppervlakken vooraan.24
- Sleep om te orbiten. **Vast renderkader tonen** toont het gele orthografische exportkader; stel per man/vrouw **Vaste renderhoogte**, **Centrum Y** (beeldcentrum) en **Pivot Y** (orbitpunt) in en sla op. Zonder handmatige pivot gebruikt de editor de rustpose-nekhoogte (of een fallback met diagnose); **Auto nek** wist een handmatige override. **Omhullende animaties** berekent eenmalig een renderhoogte en Centrum Y waarbinnen alle clips, alle frames (op project-fps) en alle 60 hoeken vallen, met 5% marge aan elke kant; de pivot blijft staan. Daarna is het kader weer vast: er is geen automatische zoom per frame, clip of laag. Equipment dat verder uitsteekt dan het lichaam vraagt handmatig extra hoogte. Body en equipment delen exact dezelfde camera. Uitvinken geeft vrije perspectief-preview met scrollzoom; die zoom verandert de export niet. Een echte 24-bits depthbuffer houdt voorste oppervlakken vooraan.25
- Kies een animatieclip en gebruik **Pauze / Speel**.25
- Kies een animatieclip en gebruik **Pauze / Speel**.26
- **Yaw − / Yaw +** volgen twaalf hoeken van 30°; **pitch -60 / -30 / 0 / 30 / 60** kiezen de hoogte. Yaw 0 kijkt naar de voorkant van de testlichamen (−Z).26
- **Yaw − / Yaw +** volgen twaalf hoeken van 30°; **pitch -60 / -30 / 0 / 30 / 60** kiezen de hoogte. Yaw 0 kijkt naar de voorkant van de testlichamen (−Z). Vaste oriëntatie (+Y omhoog, het lichaam kijkt naar −Z, zijn rechterzijde is +X): yaw 0 = voorkant (zijn rechterhand links in beeld), 90 = linkerzijde, 180 = achterkant, 270 = rechterzijde; positieve pitch kijkt van boven. Een test met gelabelde markers bewaakt deze afspraak.27
- **Render alle hoeken** rendert alle animaties × alle frames op project-fps × 60 hoeken (5 pitches × 12 yaws). Na de losse maps schrijft hij per maptype een atlas plus een `.ppda`-bestand (zie *Atlassen en .ppda*). De preview wordt direct vervangen door iedere voltooide pass: highres → detail → target/diffuse → depth → normal → specular. Alle passes behouden hun aspect met gecentreerde letterboxing en nearest-neighbor weergave. Voortgang telt echte passes; **Annuleer render** behoudt geldige gedeeltelijke uitvoer. **Live 3D** keert terug naar het model; een volgende voltooide renderpass kan de preview weer vervangen.27
- **Render alle hoeken** rendert alle animaties × alle frames op project-fps × 60 hoeken (5 pitches × 12 yaws). Na de losse maps schrijft hij per maptype een atlas plus een `.ppda`-bestand (zie *Atlassen en .ppda*). De preview wordt direct vervangen door iedere voltooide pass: highres → detail → target/diffuse → depth → normal → specular. Alle passes behouden hun aspect met gecentreerde letterboxing en nearest-neighbor weergave. Voortgang telt echte passes; **Annuleer render** behoudt geldige gedeeltelijke uitvoer. **Live 3D** keert terug naar het model; een volgende voltooide renderpass kan de preview weer vervangen.28
- Stel breedte, hoogte en fps in de Bodies-weergave in. Nieuwe projecten beginnen op 4 fps; een bestaand `.ppd` houdt zijn opgeslagen fps.28
- Stel breedte, hoogte en fps in de Bodies-weergave in. Nieuwe projecten beginnen op 4 fps; een bestaand `.ppd` houdt zijn opgeslagen fps.29
- **Zoek onderbroken runs** (ook in de equipment-editor) toont geannuleerde, mislukte of afgebroken runs van dezelfde body/equipmentset met het aantal vastgelegde PNG's. **Hervat** gaat alleen verder als bronbestanden en instellingen exact gelijk zijn aan die run; anders staat erbij wat er verschilt. Na annuleren verschijnt de run direct in deze lijst. Zie *Hervatten* hieronder.29
- **Zoek onderbroken runs** (ook in de equipment-editor) toont geannuleerde, mislukte of afgebroken runs van dezelfde body/equipmentset met het aantal vastgelegde PNG's. **Hervat** gaat alleen verder als bronbestanden en instellingen exact gelijk zijn aan die run; anders staat erbij wat er verschilt. Na annuleren verschijnt de run direct in deze lijst. Zie *Hervatten* hieronder.@@ -170,6 +170,104 @@ pub fn matrices(framing: CameraFraming, yaw: f32, pitch: f32, aspect: f32) -> (M
170
170
(preview::view_matrix(eye, target), p)
171
171
}
172
172
173
/// Labelled orientation convention (§6/§13), verified by
174
/// `orientation_convention_is_labelled`: the character faces −Z, +Y is up,
175
/// its right hand side is +X. Camera eye = `eye_offset(yaw, pitch)`.
176
pub const ORIENTATION_VIEWS: [(f32, &str); 4] = [
177
(0., "voorkant"),
178
(90., "linkerzijde"),
179
(180., "achterkant"),
180
(270., "rechterzijde"),
181
];
182
183
/// Fixed framing that contains every frame of every clip from every one of
184
/// the 60 production views (one envelope per body, never per frame/layer).
185
/// `margin` is a fraction of the envelope added on every side, `extra_m` an
186
/// absolute allowance (e.g. for equipment). Pivot stays as resolved; only
187
/// height and center Y are returned (milli). None when there is no geometry.
188
pub fn animation_envelope(
189
body: &PreviewCharacter,
190
framing: CameraFraming,
191
aspect: f32,
192
fps: u32,
193
margin: f32,
194
extra_m: f32,
195
) -> Option<(u32, i32)> {
196
let framing = resolve(body, framing).framing;
197
let pivot = framing.pivot_y_milli.unwrap_or(framing.center_y_milli) as f32 / 1000.;
198
let mut poses: Vec<Vec<[f32; 3]>> = Vec::new();
199
if body.is_static() || body.clips.is_empty() {
200
poses.push(body.positions.clone());
201
} else {
202
let step = 1. / fps.max(1) as f32;
203
for clip in &body.clips {
204
let frames = ((clip.duration / step).ceil() as usize).max(1);
205
for frame in 0..frames {
206
let bones = preview::sample_skin_matrices(body, &clip.name, frame as f32 * step);
207
poses.push(skin_points(body, &bones));
208
}
209
}
210
}
211
let (mut lo, mut hi, mut side) = (f32::INFINITY, f32::NEG_INFINITY, 0f32);
212
for pitch in preview::PAPERDOLL_PITCHES_DEG {
213
for yaw in preview::paperdoll_yaws_deg() {
214
let (view, _) = matrices(framing, yaw, pitch, aspect);
215
for pose in &poses {
216
for p in pose {
217
let x = view[0] * p[0] + view[4] * p[1] + view[8] * p[2] + view[12];
218
let y = view[1] * p[0] + view[5] * p[1] + view[9] * p[2] + view[13];
219
if x.is_finite() && y.is_finite() {
220
lo = lo.min(y);
221
hi = hi.max(y);
222
side = side.max(x.abs());
223
}
224
}
225
}
226
}
227
}
228
if !lo.is_finite() || !hi.is_finite() {
229
return None;
230
}
231
let height = (hi - lo).max(2. * side / aspect.max(1e-6));
232
let height = height * (1. + 2. * margin.max(0.)) + 2. * extra_m.max(0.);
233
let center = pivot + (hi + lo) / 2.;
234
Some((
235
(height * 1000.).ceil() as u32,
236
(center * 1000.).round() as i32,
237
))
238
}
239
240
fn skin_points(body: &PreviewCharacter, bones: &[Mat4]) -> Vec<[f32; 3]> {
241
body.positions
242
.iter()
243
.enumerate()
244
.map(|(i, p)| {
245
let w = body.weights.get(i).copied().unwrap_or([1., 0., 0., 0.]);
246
let j = body.joints.get(i).copied().unwrap_or([0; 4]);
247
let mut out = [0.; 3];
248
let mut sum = 0.;
249
for k in 0..4 {
250
if w[k] == 0. {
251
continue;
252
}
253
let m = bones
254
.get(j[k] as usize)
255
.copied()
256
.unwrap_or_else(preview::identity);
257
for a in 0..3 {
258
out[a] += w[k] * (m[a] * p[0] + m[4 + a] * p[1] + m[8 + a] * p[2] + m[12 + a]);
259
}
260
sum += w[k];
261
}
262
if sum > 0. {
263
out
264
} else {
265
*p
266
}
267
})
268
.collect()
269
}
270
173
271
/// How depth samples are stored in the depth PNGs (t980u/t981u/t983u/t987u).
174
272
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, serde::Deserialize)]
175
273
#[serde(rename_all = "snake_case")]
@@ -464,6 +562,74 @@ pub fn arm_measure(body: &PreviewCharacter) -> Option<(f32, f32)> {
464
562
465
563
#[cfg(test)]
466
564
mod tests {
565
#[test]
566
fn orientation_convention_is_labelled() {
567
// Labelled markers on a body facing −Z with its right side on +X.
568
let markers = [
569
("voorkant", [0., 1., -0.3]),
570
("achterkant", [0., 1., 0.3]),
571
("linkerzijde", [-0.3, 1., 0.]),
572
("rechterzijde", [0.3, 1., 0.]),
573
];
574
let framing = crate::project::CameraFraming::default();
575
for (yaw, label) in ORIENTATION_VIEWS {
576
let (view, _) = matrices(framing, yaw, 0., 1.);
577
// View space looks down −Z: the largest z is nearest the camera.
578
let z = |p: [f32; 3]| view[2] * p[0] + view[6] * p[1] + view[10] * p[2] + view[14];
579
let nearest = markers
580
.iter()
581
.max_by(|a, b| z(a.1).total_cmp(&z(b.1)))
582
.unwrap()
583
.0;
584
assert_eq!(nearest, label, "yaw {yaw}");
585
}
586
// Yaw 0 (front): the character's right (+X) is on the image's left.
587
let (view, _) = matrices(framing, 0., 0., 1.);
588
let x = |p: [f32; 3]| view[0] * p[0] + view[4] * p[1] + view[8] * p[2] + view[12];
589
assert!(x([0.3, 1., 0.]) < 0. && x([-0.3, 1., 0.]) > 0.);
590
// Positive pitch looks down from above: the front marker sits lower.
591
let (view, _) = matrices(framing, 0., 60., 1.);
592
let y = |p: [f32; 3]| view[1] * p[0] + view[5] * p[1] + view[9] * p[2] + view[13];
593
assert!(y([0., 1., -0.3]) < y([0., 1., 0.3]));
594
}
595
596
#[test]
597
fn animation_envelope_contains_every_frame_and_view() {
598
let body = preview::load_gltf_file(
599
&std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
600
.join("tests/fixtures/bodies/test-body-male.glb"),
601
)
602
.unwrap();
603
let start = crate::project::CameraFraming::default();
604
let (h, c) = animation_envelope(&body, start, 0.75, 8, 0., 0.).unwrap();
605
let mut framing = resolve(&body, start).framing;
606
framing.height_milli = h;
607
framing.center_y_milli = c;
608
let (m2, c2) = animation_envelope(&body, framing, 0.75, 8, 0.05, 0.1).unwrap();
609
assert!(m2 as f32 >= h as f32 * 1.1 + 199.0);
610
assert!((c2 - c).abs() <= 1);
611
// Every skinned vertex of every frame lands inside NDC [-1, 1].
612
let mut worst = 0f32;
613
for clip in &body.clips {
614
for f in 0..((clip.duration * 8.).ceil() as usize).max(1) {
615
let bones = preview::sample_skin_matrices(&body, &clip.name, f as f32 / 8.);
616
let pts = skin_points(&body, &bones);
617
for pitch in preview::PAPERDOLL_PITCHES_DEG {
618
for yaw in preview::paperdoll_yaws_deg() {
619
let (v, p) = matrices(framing, yaw, pitch, 0.75);
620
let vp = preview::mul(p, v);
621
for q in &pts {
622
let nx = vp[0] * q[0] + vp[4] * q[1] + vp[8] * q[2] + vp[12];
623
let ny = vp[1] * q[0] + vp[5] * q[1] + vp[9] * q[2] + vp[13];
624
worst = worst.max(nx.abs()).max(ny.abs());
625
}
626
}
627
}
628
}
629
}
630
assert!(worst <= 1.001 && worst > 0.95, "worst NDC {worst}");
631
}
632
467
633
use super::*;
468
634
use crate::preview::{self, PreviewCharacter, SceneNode};
469
635
@@ -170,6 +170,104 @@ pub fn matrices(framing: CameraFraming, yaw: f32, pitch: f32, aspect: f32) -> (M170
(preview::view_matrix(eye, target), p)170
(preview::view_matrix(eye, target), p)171
}171
}172
172
173
/// Labelled orientation convention (§6/§13), verified by174
/// `orientation_convention_is_labelled`: the character faces −Z, +Y is up,175
/// its right hand side is +X. Camera eye = `eye_offset(yaw, pitch)`.176
pub const ORIENTATION_VIEWS: [(f32, &str); 4] = [177
(0., "voorkant"),178
(90., "linkerzijde"),179
(180., "achterkant"),180
(270., "rechterzijde"),181
];182
183
/// Fixed framing that contains every frame of every clip from every one of184
/// the 60 production views (one envelope per body, never per frame/layer).185
/// `margin` is a fraction of the envelope added on every side, `extra_m` an186
/// absolute allowance (e.g. for equipment). Pivot stays as resolved; only187
/// height and center Y are returned (milli). None when there is no geometry.188
pub fn animation_envelope(189
body: &PreviewCharacter,190
framing: CameraFraming,191
aspect: f32,192
fps: u32,193
margin: f32,194
extra_m: f32,195
) -> Option<(u32, i32)> {196
let framing = resolve(body, framing).framing;197
let pivot = framing.pivot_y_milli.unwrap_or(framing.center_y_milli) as f32 / 1000.;198
let mut poses: Vec<Vec<[f32; 3]>> = Vec::new();199
if body.is_static() || body.clips.is_empty() {200
poses.push(body.positions.clone());201
} else {202
let step = 1. / fps.max(1) as f32;203
for clip in &body.clips {204
let frames = ((clip.duration / step).ceil() as usize).max(1);205
for frame in 0..frames {206
let bones = preview::sample_skin_matrices(body, &clip.name, frame as f32 * step);207
poses.push(skin_points(body, &bones));208
}209
}210
}211
let (mut lo, mut hi, mut side) = (f32::INFINITY, f32::NEG_INFINITY, 0f32);212
for pitch in preview::PAPERDOLL_PITCHES_DEG {213
for yaw in preview::paperdoll_yaws_deg() {214
let (view, _) = matrices(framing, yaw, pitch, aspect);215
for pose in &poses {216
for p in pose {217
let x = view[0] * p[0] + view[4] * p[1] + view[8] * p[2] + view[12];218
let y = view[1] * p[0] + view[5] * p[1] + view[9] * p[2] + view[13];219
if x.is_finite() && y.is_finite() {220
lo = lo.min(y);221
hi = hi.max(y);222
side = side.max(x.abs());223
}224
}225
}226
}227
}228
if !lo.is_finite() || !hi.is_finite() {229
return None;230
}231
let height = (hi - lo).max(2. * side / aspect.max(1e-6));232
let height = height * (1. + 2. * margin.max(0.)) + 2. * extra_m.max(0.);233
let center = pivot + (hi + lo) / 2.;234
Some((235
(height * 1000.).ceil() as u32,236
(center * 1000.).round() as i32,237
))238
}239
240
fn skin_points(body: &PreviewCharacter, bones: &[Mat4]) -> Vec<[f32; 3]> {241
body.positions242
.iter()243
.enumerate()244
.map(|(i, p)| {245
let w = body.weights.get(i).copied().unwrap_or([1., 0., 0., 0.]);246
let j = body.joints.get(i).copied().unwrap_or([0; 4]);247
let mut out = [0.; 3];248
let mut sum = 0.;249
for k in 0..4 {250
if w[k] == 0. {251
continue;252
}253
let m = bones254
.get(j[k] as usize)255
.copied()256
.unwrap_or_else(preview::identity);257
for a in 0..3 {258
out[a] += w[k] * (m[a] * p[0] + m[4 + a] * p[1] + m[8 + a] * p[2] + m[12 + a]);259
}260
sum += w[k];261
}262
if sum > 0. {263
out264
} else {265
*p266
}267
})268
.collect()269
}270
173
/// How depth samples are stored in the depth PNGs (t980u/t981u/t983u/t987u).271
/// How depth samples are stored in the depth PNGs (t980u/t981u/t983u/t987u).174
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, serde::Deserialize)]272
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, serde::Deserialize)]175
#[serde(rename_all = "snake_case")]273
#[serde(rename_all = "snake_case")]@@ -464,6 +562,74 @@ pub fn arm_measure(body: &PreviewCharacter) -> Option<(f32, f32)> {464
562
465
#[cfg(test)]563
#[cfg(test)]466
mod tests {564
mod tests {565
#[test]566
fn orientation_convention_is_labelled() {567
// Labelled markers on a body facing −Z with its right side on +X.568
let markers = [569
("voorkant", [0., 1., -0.3]),570
("achterkant", [0., 1., 0.3]),571
("linkerzijde", [-0.3, 1., 0.]),572
("rechterzijde", [0.3, 1., 0.]),573
];574
let framing = crate::project::CameraFraming::default();575
for (yaw, label) in ORIENTATION_VIEWS {576
let (view, _) = matrices(framing, yaw, 0., 1.);577
// View space looks down −Z: the largest z is nearest the camera.578
let z = |p: [f32; 3]| view[2] * p[0] + view[6] * p[1] + view[10] * p[2] + view[14];579
let nearest = markers580
.iter()581
.max_by(|a, b| z(a.1).total_cmp(&z(b.1)))582
.unwrap()583
.0;584
assert_eq!(nearest, label, "yaw {yaw}");585
}586
// Yaw 0 (front): the character's right (+X) is on the image's left.587
let (view, _) = matrices(framing, 0., 0., 1.);588
let x = |p: [f32; 3]| view[0] * p[0] + view[4] * p[1] + view[8] * p[2] + view[12];589
assert!(x([0.3, 1., 0.]) < 0. && x([-0.3, 1., 0.]) > 0.);590
// Positive pitch looks down from above: the front marker sits lower.591
let (view, _) = matrices(framing, 0., 60., 1.);592
let y = |p: [f32; 3]| view[1] * p[0] + view[5] * p[1] + view[9] * p[2] + view[13];593
assert!(y([0., 1., -0.3]) < y([0., 1., 0.3]));594
}595
596
#[test]597
fn animation_envelope_contains_every_frame_and_view() {598
let body = preview::load_gltf_file(599
&std::path::Path::new(env!("CARGO_MANIFEST_DIR"))600
.join("tests/fixtures/bodies/test-body-male.glb"),601
)602
.unwrap();603
let start = crate::project::CameraFraming::default();604
let (h, c) = animation_envelope(&body, start, 0.75, 8, 0., 0.).unwrap();605
let mut framing = resolve(&body, start).framing;606
framing.height_milli = h;607
framing.center_y_milli = c;608
let (m2, c2) = animation_envelope(&body, framing, 0.75, 8, 0.05, 0.1).unwrap();609
assert!(m2 as f32 >= h as f32 * 1.1 + 199.0);610
assert!((c2 - c).abs() <= 1);611
// Every skinned vertex of every frame lands inside NDC [-1, 1].612
let mut worst = 0f32;613
for clip in &body.clips {614
for f in 0..((clip.duration * 8.).ceil() as usize).max(1) {615
let bones = preview::sample_skin_matrices(&body, &clip.name, f as f32 / 8.);616
let pts = skin_points(&body, &bones);617
for pitch in preview::PAPERDOLL_PITCHES_DEG {618
for yaw in preview::paperdoll_yaws_deg() {619
let (v, p) = matrices(framing, yaw, pitch, 0.75);620
let vp = preview::mul(p, v);621
for q in &pts {622
let nx = vp[0] * q[0] + vp[4] * q[1] + vp[8] * q[2] + vp[12];623
let ny = vp[1] * q[0] + vp[5] * q[1] + vp[9] * q[2] + vp[13];624
worst = worst.max(nx.abs()).max(ny.abs());625
}626
}627
}628
}629
}630
assert!(worst <= 1.001 && worst > 0.95, "worst NDC {worst}");631
}632
467
use super::*;633
use super::*;468
use crate::preview::{self, PreviewCharacter, SceneNode};634
use crate::preview::{self, PreviewCharacter, SceneNode};469
635
@@ -931,6 +931,9 @@ impl EditorApp {
931
931
}
932
932
};
933
933
if let Some(opened) = &mut self.session.opened {
934
let aspect = opened.document.render.target_width.max(1) as f32
935
/ opened.document.render.target_height.max(1) as f32;
936
let fps = opened.document.render.fps;
934
937
let camera = &mut opened.document.render.framing[sex];
935
938
let mut height = camera.height_milli as f32 / 1000.;
936
939
let mut center = camera.center_y_milli as f32 / 1000.;
@@ -988,6 +991,27 @@ impl EditorApp {
988
991
{
989
992
camera.pivot_y_milli = None;
990
993
}
994
let body = match sex {
995
0 => self.preview_male.as_ref(),
996
_ => self.preview_female.as_ref(),
997
};
998
if let Some(body) = body {
999
if ui
1000
.button("Omhullende animaties")
1001
.on_hover_text(
1002
"Eenmalig: hoogte en centrum zo dat alle clips, frames en 60 hoeken \
1003
binnen het kader vallen (+5% marge). Daarna weer vast; geen autozoom.",
1004
)
1005
.clicked()
1006
{
1007
if let Some((h, c)) =
1008
crate::camera::animation_envelope(body, *camera, aspect, fps, 0.05, 0.)
1009
{
1010
camera.height_milli = h.clamp(100, 10_000);
1011
camera.center_y_milli = c.clamp(-10_000, 10_000);
1012
}
1013
}
1014
}
991
1015
ui.checkbox(&mut self.show_render_frame, "Vast renderkader tonen");
992
1016
});
993
1017
let auto_range = diagnostic
@@ -931,6 +931,9 @@ impl EditorApp {931
}931
}932
};932
};933
if let Some(opened) = &mut self.session.opened {933
if let Some(opened) = &mut self.session.opened {934
let aspect = opened.document.render.target_width.max(1) as f32935
/ opened.document.render.target_height.max(1) as f32;936
let fps = opened.document.render.fps;934
let camera = &mut opened.document.render.framing[sex];937
let camera = &mut opened.document.render.framing[sex];935
let mut height = camera.height_milli as f32 / 1000.;938
let mut height = camera.height_milli as f32 / 1000.;936
let mut center = camera.center_y_milli as f32 / 1000.;939
let mut center = camera.center_y_milli as f32 / 1000.;@@ -988,6 +991,27 @@ impl EditorApp {988
{991
{989
camera.pivot_y_milli = None;992
camera.pivot_y_milli = None;990
}993
}994
let body = match sex {995
0 => self.preview_male.as_ref(),996
_ => self.preview_female.as_ref(),997
};998
if let Some(body) = body {999
if ui1000
.button("Omhullende animaties")1001
.on_hover_text(1002
"Eenmalig: hoogte en centrum zo dat alle clips, frames en 60 hoeken \1003
binnen het kader vallen (+5% marge). Daarna weer vast; geen autozoom.",1004
)1005
.clicked()1006
{1007
if let Some((h, c)) =1008
crate::camera::animation_envelope(body, *camera, aspect, fps, 0.05, 0.)1009
{1010
camera.height_milli = h.clamp(100, 10_000);1011
camera.center_y_milli = c.clamp(-10_000, 10_000);1012
}1013
}1014
}991
ui.checkbox(&mut self.show_render_frame, "Vast renderkader tonen");1015
ui.checkbox(&mut self.show_render_frame, "Vast renderkader tonen");992
});1016
});993
let auto_range = diagnostic1017
let auto_range = diagnostic