@@ -198,3 +198,258 @@ pub fn compute_model_view_projection_matrix_about_pivot(
198198 ) ;
199199 projection. multiply ( & view) . multiply ( & model)
200200}
201+
202+ #[ cfg( test) ]
203+ mod tests {
204+ use super :: * ;
205+ use crate :: math:: matrix as m;
206+
207+ /// This test demonstrates the complete order of operations used to produce a
208+ /// model matrix. It rotates a base identity matrix by a chosen axis and
209+ /// angle, applies a uniform scale using a diagonal scaling matrix, and then
210+ /// applies a world translation. The expected matrix is built step by step in
211+ /// the same manner so that individual differences are easy to reason about
212+ /// when reading a failure. Every element is compared using a small tolerance
213+ /// to account for floating point rounding.
214+ #[ test]
215+ fn model_matrix_composes_rotation_scale_and_translation ( ) {
216+ let translation = [ 3.0 , -2.0 , 5.0 ] ;
217+ let axis = [ 0.0 , 1.0 , 0.0 ] ;
218+ let angle_in_turns = 0.25 ; // quarter turn
219+ let scale = 2.0 ;
220+
221+ // Compute via the public function under test.
222+ let actual = compute_model_matrix ( translation, axis, angle_in_turns, scale) ;
223+
224+ // Build the expected matrix explicitly: R, then S, then T.
225+ let mut expected: [ [ f32 ; 4 ] ; 4 ] = m:: identity_matrix ( 4 , 4 ) ;
226+ expected = m:: rotate_matrix ( expected, axis, angle_in_turns) ;
227+
228+ let mut s: [ [ f32 ; 4 ] ; 4 ] = [ [ 0.0 ; 4 ] ; 4 ] ;
229+ for i in 0 ..4 {
230+ for j in 0 ..4 {
231+ s[ i] [ j] = if i == j {
232+ if i == 3 {
233+ 1.0
234+ } else {
235+ scale
236+ }
237+ } else {
238+ 0.0
239+ } ;
240+ }
241+ }
242+ expected = expected. multiply ( & s) ;
243+
244+ let t: [ [ f32 ; 4 ] ; 4 ] = m:: translation_matrix ( translation) ;
245+ let expected = t. multiply ( & expected) ;
246+
247+ for i in 0 ..4 {
248+ for j in 0 ..4 {
249+ crate :: assert_approximately_equal!(
250+ actual. at( i, j) ,
251+ expected. at( i, j) ,
252+ 1e-5
253+ ) ;
254+ }
255+ }
256+ }
257+
258+ /// This test verifies that rotating and scaling around a local pivot point
259+ /// produces the same result as translating into the pivot, applying the base
260+ /// transform, and translating back out, followed by the world translation.
261+ /// It constructs both forms and checks that all elements match within a
262+ /// small tolerance.
263+ #[ test]
264+ fn model_matrix_respects_local_pivot ( ) {
265+ let translation = [ 1.0 , 2.0 , 3.0 ] ;
266+ let axis = [ 1.0 , 0.0 , 0.0 ] ;
267+ let angle_in_turns = 0.125 ; // one eighth of a full turn
268+ let scale = 0.5 ;
269+ let pivot = [ 10.0 , -4.0 , 2.0 ] ;
270+
271+ let actual = compute_model_matrix_about_pivot (
272+ translation,
273+ axis,
274+ angle_in_turns,
275+ scale,
276+ pivot,
277+ ) ;
278+
279+ let base =
280+ compute_model_matrix ( [ 0.0 , 0.0 , 0.0 ] , axis, angle_in_turns, scale) ;
281+ let to_pivot: [ [ f32 ; 4 ] ; 4 ] = m:: translation_matrix ( pivot) ;
282+ let from_pivot: [ [ f32 ; 4 ] ; 4 ] =
283+ m:: translation_matrix ( [ -pivot[ 0 ] , -pivot[ 1 ] , -pivot[ 2 ] ] ) ;
284+ let world: [ [ f32 ; 4 ] ; 4 ] = m:: translation_matrix ( translation) ;
285+ let expected = world
286+ . multiply ( & to_pivot)
287+ . multiply ( & base)
288+ . multiply ( & from_pivot) ;
289+
290+ for i in 0 ..4 {
291+ for j in 0 ..4 {
292+ crate :: assert_approximately_equal!(
293+ actual. at( i, j) ,
294+ expected. at( i, j) ,
295+ 1e-5
296+ ) ;
297+ }
298+ }
299+ }
300+
301+ /// This test confirms that the view computation is the inverse of a camera
302+ /// translation. For a camera expressed only as a world space translation, the
303+ /// inverse is a translation by the negated vector. The test constructs that
304+ /// expected matrix directly and compares it to the function result.
305+ #[ test]
306+ fn view_matrix_is_inverse_translation ( ) {
307+ let camera_position = [ 7.0 , -3.0 , 2.5 ] ;
308+ let expected: [ [ f32 ; 4 ] ; 4 ] = m:: translation_matrix ( [
309+ -camera_position[ 0 ] ,
310+ -camera_position[ 1 ] ,
311+ -camera_position[ 2 ] ,
312+ ] ) ;
313+ let actual = compute_view_matrix ( camera_position) ;
314+ assert_eq ! ( actual, expected) ;
315+ }
316+
317+ /// This test validates that the perspective projection matches an
318+ /// OpenGL‑style projection that is converted into the normalized device
319+ /// coordinate range used by the target platforms. The expected conversion is
320+ /// performed by multiplying a fixed conversion matrix with the projection
321+ /// produced by the existing matrix helper. The result is compared element by
322+ /// element within a small tolerance.
323+ #[ test]
324+ fn perspective_projection_matches_converted_reference ( ) {
325+ let fov_turns = 0.25 ;
326+ let width = 1280 ;
327+ let height = 720 ;
328+ let near = 0.1 ;
329+ let far = 100.0 ;
330+
331+ let actual =
332+ compute_perspective_projection ( fov_turns, width, height, near, far) ;
333+
334+ let aspect = width as f32 / height as f32 ;
335+ let projection_gl: [ [ f32 ; 4 ] ; 4 ] =
336+ m:: perspective_matrix ( fov_turns, aspect, near, far) ;
337+ let conversion = [
338+ [ 1.0 , 0.0 , 0.0 , 0.0 ] ,
339+ [ 0.0 , 1.0 , 0.0 , 0.0 ] ,
340+ [ 0.0 , 0.0 , 0.5 , 0.0 ] ,
341+ [ 0.0 , 0.0 , 0.5 , 1.0 ] ,
342+ ] ;
343+ let expected = conversion. multiply ( & projection_gl) ;
344+
345+ for i in 0 ..4 {
346+ for j in 0 ..4 {
347+ crate :: assert_approximately_equal!(
348+ actual. at( i, j) ,
349+ expected. at( i, j) ,
350+ 1e-5
351+ ) ;
352+ }
353+ }
354+ }
355+
356+ /// This test builds a full model, view, and projection composition using both
357+ /// the public helper and a reference expression that multiplies the same
358+ /// parts in the same order. It uses a simple camera and a non‑trivial model
359+ /// transform to provide coverage for the code paths. Results are compared
360+ /// with a small tolerance to account for floating point rounding.
361+ #[ test]
362+ fn model_view_projection_composition_matches_reference ( ) {
363+ let camera = SimpleCamera {
364+ position : [ 0.5 , -1.0 , 2.0 ] ,
365+ field_of_view_in_turns : 0.3 ,
366+ near_clipping_plane : 0.01 ,
367+ far_clipping_plane : 500.0 ,
368+ } ;
369+ let ( w, h) = ( 1024 , 600 ) ;
370+ let model_t = [ 2.0 , 0.5 , -3.0 ] ;
371+ let axis = [ 0.0 , 0.0 , 1.0 ] ;
372+ let angle = 0.2 ;
373+ let scale = 1.25 ;
374+
375+ let actual = compute_model_view_projection_matrix (
376+ & camera, w, h, model_t, axis, angle, scale,
377+ ) ;
378+
379+ let model = compute_model_matrix ( model_t, axis, angle, scale) ;
380+ let view = compute_view_matrix ( camera. position ) ;
381+ let proj = compute_perspective_projection (
382+ camera. field_of_view_in_turns ,
383+ w,
384+ h,
385+ camera. near_clipping_plane ,
386+ camera. far_clipping_plane ,
387+ ) ;
388+ let expected = proj. multiply ( & view) . multiply ( & model) ;
389+
390+ for i in 0 ..4 {
391+ for j in 0 ..4 {
392+ crate :: assert_approximately_equal!(
393+ actual. at( i, j) ,
394+ expected. at( i, j) ,
395+ 1e-5
396+ ) ;
397+ }
398+ }
399+ }
400+
401+ /// This test builds a full model, view, and projection composition for a
402+ /// model that rotates and scales around a local pivot point. It compares the
403+ /// public helper result to a reference expression that expands the pivot
404+ /// operations into individual translations and the base transform. Elements
405+ /// are compared with a small tolerance to make the test robust to floating
406+ /// point differences.
407+ #[ test]
408+ fn model_view_projection_about_pivot_matches_reference ( ) {
409+ let camera = SimpleCamera {
410+ position : [ -3.0 , 0.0 , 1.0 ] ,
411+ field_of_view_in_turns : 0.15 ,
412+ near_clipping_plane : 0.1 ,
413+ far_clipping_plane : 50.0 ,
414+ } ;
415+ let ( w, h) = ( 800 , 480 ) ;
416+ let model_t = [ 0.0 , -1.0 , 2.0 ] ;
417+ let axis = [ 0.0 , 1.0 , 0.0 ] ;
418+ let angle = 0.4 ;
419+ let scale = 0.75 ;
420+ let pivot = [ 5.0 , 0.0 , -2.0 ] ;
421+
422+ let actual = compute_model_view_projection_matrix_about_pivot (
423+ & camera, w, h, model_t, axis, angle, scale, pivot,
424+ ) ;
425+
426+ let base = compute_model_matrix ( [ 0.0 , 0.0 , 0.0 ] , axis, angle, scale) ;
427+ let to_pivot: [ [ f32 ; 4 ] ; 4 ] = m:: translation_matrix ( pivot) ;
428+ let from_pivot: [ [ f32 ; 4 ] ; 4 ] =
429+ m:: translation_matrix ( [ -pivot[ 0 ] , -pivot[ 1 ] , -pivot[ 2 ] ] ) ;
430+ let world: [ [ f32 ; 4 ] ; 4 ] = m:: translation_matrix ( model_t) ;
431+ let model = world
432+ . multiply ( & to_pivot)
433+ . multiply ( & base)
434+ . multiply ( & from_pivot) ;
435+ let view = compute_view_matrix ( camera. position ) ;
436+ let proj = compute_perspective_projection (
437+ camera. field_of_view_in_turns ,
438+ w,
439+ h,
440+ camera. near_clipping_plane ,
441+ camera. far_clipping_plane ,
442+ ) ;
443+ let expected = proj. multiply ( & view) . multiply ( & model) ;
444+
445+ for i in 0 ..4 {
446+ for j in 0 ..4 {
447+ crate :: assert_approximately_equal!(
448+ actual. at( i, j) ,
449+ expected. at( i, j) ,
450+ 1e-5
451+ ) ;
452+ }
453+ }
454+ }
455+ }
0 commit comments