1%%
2%%  e3d_transf.erl --
3%%
4%%     More transformation matrix utilities
5%%
6%%  Copyright (c) 2010-2011 Dan Gudmundsson
7%%
8%%  See the file "license.terms" for information on usage and redistribution
9%%  of this file, and for a DISCLAIMER OF ALL WARRANTIES.
10%%
11%%     $Id$
12%%%
13%%% @doc   More transformation matrix utilities
14%%%
15%%%         All of the matrix operations operates in the following order
16%%%          I = identity(),
17%%%          T = translate(I, Vec),
18%%%          R = rotate(T, Vec),
19%%%          Matrix = Rotate(Translate())
20%%%
21%%%         Also using opengl right handed coordinate system
22%%% @end
23-module(e3d_transform).
24
25-export([%% Initializes matrices
26	 identity/0, init/1,
27	 lookat/3, ortho/2, ortho/6, perspective/3, perspective/4, pick/5, frustum/6,
28	 %% Get the actual matrices
29	 matrix/1, inv_matrix/1,
30	 %% Transform the matrices
31	 inverse/1, translate/2, rotate/2, rotate/3, scale/2, mul/1, mul/2,
32         %% (un)Projection
33         project/4, unproject/4
34	]).
35
36-include("e3d.hrl").
37
38-type transform() :: #e3d_transf{}.
39
40-type matrix() :: e3d_mat:matrix().
41-type vector() :: e3d_vec:vector().
42-type point() :: e3d_vec:point().
43
44
45%%%-------------------------------------------------------------------
46%%--------------------------------------------------------------------
47%% @doc  Returns the identity transform
48%% @end
49%%--------------------------------------------------------------------
50-spec identity() -> transform().
51identity() ->
52    #e3d_transf{}.
53
54%%--------------------------------------------------------------------
55%% @doc  Initializes transform from matrix mat
56%% @end
57%%--------------------------------------------------------------------
58-spec init(matrix()) -> transform().
59init(Mat) when tuple_size(Mat) =:= 12 ->
60    init(e3d_mat:expand(Mat));
61init(Mat) ->
62    #e3d_transf{mat=Mat, inv=e3d_mat:invert(Mat)}.
63
64%%--------------------------------------------------------------------
65%% @doc  Returns the matrix
66%% @end
67%%--------------------------------------------------------------------
68-spec matrix(transform()) -> matrix().
69matrix(#e3d_transf{mat=M}) -> e3d_mat:expand(M).
70
71%%--------------------------------------------------------------------
72%% @doc  Returns the inverse matrix
73%% @end
74%%--------------------------------------------------------------------
75-spec inv_matrix(transform()) -> matrix().
76inv_matrix(#e3d_transf{inv=I}) -> e3d_mat:expand(I).
77
78%%%-------------------------------------------------------------------
79
80
81%%--------------------------------------------------------------------
82%% @doc  Inverses the transform
83%% @end
84%%--------------------------------------------------------------------
85-spec inverse(transform()) -> transform().
86inverse(#e3d_transf{mat=M, inv=I}) ->
87    #e3d_transf{mat=I, inv=M}.
88
89%%--------------------------------------------------------------------
90%% @doc  Translates the matrix with vector
91%% @end
92%%--------------------------------------------------------------------
93-spec translate(transform(), vector()) -> transform().
94translate(#e3d_transf{mat=M,inv=I}, {Dx,Dy,Dz}) ->
95    #e3d_transf{mat = e3d_mat:mul(M, e3d_mat:translate(Dx,Dy,Dz)),
96		inv = e3d_mat:mul(e3d_mat:translate(-Dx,-Dy,-Dz), I)}.
97
98%%--------------------------------------------------------------------
99%% @doc  Rotates the matrix with rotation matrix
100%% @end
101%%--------------------------------------------------------------------
102-spec rotate(transform(), matrix()) -> transform().
103rotate(#e3d_transf{mat=M,inv=I}, Rot)
104  when tuple_size(Rot) =:= 12; tuple_size(Rot) =:= 16 ->
105    #e3d_transf{mat = e3d_mat:mul(M, Rot),
106		inv = e3d_mat:mul(e3d_mat:transpose(Rot), I)}.
107
108%%--------------------------------------------------------------------
109%% @doc  Rotates the matrix with angle (in degrees) and direction
110%% @end
111%%--------------------------------------------------------------------
112-spec rotate(transform(), number(), vector()) -> transform().
113rotate(Mat = #e3d_transf{}, A, Vec) ->
114    rotate(Mat, e3d_mat:rotate(A,Vec)).
115
116%%--------------------------------------------------------------------
117%% @doc  Scales the matrix with {ScaleX, ScaleY, ScaleZ}
118%% @end
119%%--------------------------------------------------------------------
120-spec scale(transform(), vector()) -> transform().
121scale(#e3d_transf{mat=M,inv=I}, {X,Y,Z}) ->
122    #e3d_transf{mat = e3d_mat:mul(M, e3d_mat:scale(X,Y,Z)),
123		inv = e3d_mat:mul(e3d_mat:scale(1/X,1/Y,1/Z), I)}.
124
125%%----------------------------------------------------------------------
126%% @doc  Multiplies the current matrix (at right) with new Mat (at left)
127%%       Trans(Vec) = Mat(Current(Vec))
128%% @end
129%%----------------------------------------------------------------------
130-spec mul(transform(), transform()) -> transform().
131mul(#e3d_transf{mat=M1,inv=I1}, #e3d_transf{mat=M2,inv=I2}) ->
132    #e3d_transf{mat = e3d_mat:mul(M1, M2), inv = e3d_mat:mul(I2, I1)}.
133
134%%--------------------------------------------------------------
135%% mul([Rx,Ry,Rz]) = mul([mul(Ry,Rx),Rz])
136%%--------------------------------------------------------------
137-spec mul([transform()]) -> transform().
138mul([#e3d_transf{}=A,#e3d_transf{}=B | T ]) -> mul([mul(B,A) | T]);
139mul([#e3d_transf{}=A]) -> A.
140
141%%----------------------------------------------------------------------
142%% @doc  Transforms point to window coordinates
143%%
144%% @end
145%%----------------------------------------------------------------------
146-spec project(Point::point(),
147              ModelView::transform(),
148              Projection::transform(), ViewPort::{integer(), integer(), integer(), integer()}) ->
149          point().
150project(Point, #e3d_transf{mat=MV}, #e3d_transf{mat=Pr}, {V0,V1,V2,V3}) ->
151    P0 = e3d_mat:mul_point(MV, Point),
152    P1 = e3d_mat:mul_point(Pr, P0),
153    {Px,Py,Pz} = e3d_vec:add_prod({0.5,0.5,0.5}, P1, 0.5),
154    {Px*V2+V0, Py*V3+V1, Pz}.
155
156%%----------------------------------------------------------------------
157%% @doc  Maps windows coordinates to object coordinates
158%%
159%% @end
160%%----------------------------------------------------------------------
161-spec unproject(WinPoint::e3d_vec:point(),
162                ModelView::transform(),
163                Projection::transform(), ViewPort::{integer(), integer(), integer(), integer()}) ->
164          e3d_vec:point().
165unproject({WinX,WinY,WinZ}, #e3d_transf{inv=InvMV}, #e3d_transf{inv=InvPr}, {V0,V1,V2,V3}) ->
166    %% From windows coords to 0.0-1.0
167    Point0 = {(WinX-V0)/V2, (WinY-V1)/V3, WinZ},
168    %% From 0.0-1.0 -> -1, 1
169    Point = e3d_vec:add_prod({-1.0,-1.0,-1.0}, Point0, 2.0),
170    %% ModelView Projection matrix
171    MVPM = e3d_mat:mul(InvMV,InvPr),
172    e3d_mat:mul_point(MVPM, Point).
173
174%%%-------------------------------------------------------------------
175
176%%--------------------------------------------------------------------
177%% @doc  Generates a world to camera transformation
178%% @end
179%%--------------------------------------------------------------------
180-spec lookat(point(), vector(), vector()) -> transform().
181lookat(Pos, Look, Up) ->
182    Dir = e3d_vec:norm_sub(Look, Pos),
183    Right = e3d_vec:norm(e3d_vec:cross(Dir, e3d_vec:norm(Up))),
184    NewUp = e3d_vec:norm(e3d_vec:cross(Right, Dir)),
185    AsList = [tuple_to_list(Right),             0.0,
186	      tuple_to_list(NewUp),             0.0,
187	      tuple_to_list(e3d_vec:neg(Dir)),  0.0,
188	      0.0, 0.0, 0.0,                    1.0],
189    CamToWorld = list_to_tuple(lists:flatten(AsList)),
190    WorldToCam = e3d_mat:invert(CamToWorld),
191    translate(#e3d_transf{mat=WorldToCam,inv=CamToWorld}, e3d_vec:neg(Pos)).
192
193%%--------------------------------------------------------------------
194%% @doc  Generates a ortho transformation
195%% @end
196%%--------------------------------------------------------------------
197-spec ortho(float(), float()) -> transform().
198ortho(Near, Far) ->
199    ortho(-1.0, 1.0, -1.0, 1.0, Near, Far).
200
201-spec ortho(float(), float(), float(), float(), float(), float()) -> transform().
202ortho(Left, Right, Bottom, Top, Near, Far) ->
203    O = 0.0,
204    IDx = 1/(Right-Left),
205    IDy = 1/(Top-Bottom),
206    IDz = 1/(Far-Near),
207
208    Mat0 = {2.0, O, O,
209	    O, 2.0, O,
210	    O,   O, -2.0,
211	    O,   O, O},
212    %% Do this in 3 steps to avoid inverse calculation problems
213    Mat1 = scale(init(Mat0), {IDx,IDy,IDz}),
214    Tv = {-(Right+Left)*IDx, -(Top+Bottom)*IDy, -(Far+Near)*IDz},
215    Trans = translate(identity(), Tv),
216    mul(Trans, Mat1).
217
218%%--------------------------------------------------------------------
219%% @doc  Generates a perspective transformation
220%%       Fov = Field Of View (in degrees)
221%%       Projects from camera space: Z = {-near, -far}
222%%                  to screen space: Z' = {0.0, 1.0}
223%% @end
224%%--------------------------------------------------------------------
225-spec perspective(Fov::float(),
226		  Near::float(), Far::float()) -> transform().
227perspective(Fov, Near, Far) ->
228    perspective(Fov, 1.0, Near, Far).
229
230-spec perspective(Fov::float(), Aspect::float(),
231		  Near::float(), Far::float()) -> transform().
232perspective(Fov, Aspect, Near, Far) ->
233    T = 1.0 / math:tan((Fov*math:pi()/180)/2.0),
234    %% Perform projective divide
235    D = 1.0 / (Far-Near),  %% Inverted Denom
236    I = 1.0, O = 0.0,
237    Persp = {I/Aspect, O,               O,  O,
238	     O,        I,               O,  O,
239	     O,        O,   -(Near+Far)*D, -I,
240	     O,        O, -2.0*Far*Near*D,  O},
241    InvPersp = e3d_mat:invert(Persp),
242    scale(#e3d_transf{mat=Persp, inv=InvPersp}, {T,T,1.0}).
243
244%%--------------------------------------------------------------------
245%% @doc  Generates a perspective projection matrix
246%% @end
247%%--------------------------------------------------------------------
248
249-spec frustum(Left::float(), Right::float(),
250              Bottom::float(), Top::float(),
251              Near::float(), Far::float()) ->
252          transform().
253frustum(Left, Right, Bottom, Top, Near, Far) ->
254    InvX = 1/(Right-Left),
255    InvY = 1/(Top-Bottom),
256    InvZ = 1/(Far-Near),
257    A = (Right+Left)*InvX,
258    B = (Top+Bottom)*InvY,
259    C = -(Far+Near)*InvZ,
260    D = -(2*Far*Near)*InvZ,
261    I = 1.0, O = 0.0,
262    E = 2*Near*InvX,
263    F = 2*Near*InvY,
264
265    Persp = {E,   O,   O,  O,
266	     O,   F,   O,  O,
267	     A,   B,   C, -I,
268	     O,   O,   D,  O},
269    InvPersp = e3d_mat:invert(Persp),
270    #e3d_transf{mat=Persp, inv=InvPersp}.
271
272%%--------------------------------------------------------------------
273%% @doc  Generates a pick matrix transformation
274%%       Equiv to glu:pickMatrix/5
275%% @end
276%%--------------------------------------------------------------------
277-spec pick(X::float(), Y::float(),
278	   Width::float(), Height::float(),
279	   Viewport::{integer(),integer(),integer(),integer()}
280	  ) -> transform().
281pick(X, Y, W, H, {X0,Y0,X1,Y1}) ->
282    Sx = X1 / W,
283    Sy = Y1 / H,
284    Tx = (X1+2.0*(X0-X)) / W,
285    Ty = (Y1+2.0*(Y0-Y)) / H,
286    I = 1.0, O = 0.0,
287    Pick = {Sx, O, O, O,
288	    O, Sy, O, O,
289	    O,  O, I, O,
290	    Tx,Ty, O, I},
291    init(Pick).
292