|
| 1 | +%% This is an example. Feel free to copy and reuse as you wish. |
| 2 | +%% |
| 3 | +%% This example was almost entirely generated by Grok 3 with |
| 4 | +%% minor human-driven changes (such as fixing the position of |
| 5 | +%% legs). The raster is imperfect, either missing sides or |
| 6 | +%% sides being incorrectly defined. |
| 7 | +%% |
| 8 | +%% The conversation leading to this example can be found at |
| 9 | +%% https://x.com/i/grok/share/IN1mWW5a4g1zcqYTInA4X8BXP |
| 10 | + |
| 11 | +-module(duck_engine). |
| 12 | +-export([run/0]). |
| 13 | + |
| 14 | +run() -> |
| 15 | + % Initialize SDL with video subsystem |
| 16 | + case sdl:start([video]) of |
| 17 | + ok -> |
| 18 | + % Ensure SDL stops when this process exits |
| 19 | + sdl:stop_on_exit(), |
| 20 | + % Create a window (title, x, y, width, height, flags) |
| 21 | + {ok, Window} = sdl_window:create(<<"Duck Engine">>, 100, 100, 800, 600, []), |
| 22 | + % Create a renderer with acceleration and vsync |
| 23 | + {ok, Renderer} = sdl_renderer:create(Window, -1, [accelerated, present_vsync]), |
| 24 | + % Define initial duck vertices |
| 25 | + Duck = duck_vertices(), |
| 26 | + % Start the rendering loop with initial rotation angles |
| 27 | + loop(Window, Renderer, Duck, {0.0, 0.0, 0.0}); |
| 28 | + {application_start_error, Reason} -> |
| 29 | + io:format("Failed to start esdl2 application: ~p~n", [Reason]), |
| 30 | + error; |
| 31 | + {error, String} -> |
| 32 | + io:format("SDL initialization failed: ~s~n", [String]), |
| 33 | + error |
| 34 | + end. |
| 35 | + |
| 36 | +% Main rendering loop |
| 37 | +loop(Window, Renderer, Duck, {XAngle, YAngle, ZAngle}) -> |
| 38 | + % Set clear color to white (R, G, B, A) |
| 39 | + sdl_renderer:set_draw_color(Renderer, 255, 255, 255, 255), |
| 40 | + % Clear the renderer |
| 41 | + sdl_renderer:clear(Renderer), |
| 42 | + % Rotate the duck vertices |
| 43 | + RotatedDuck = rotate_duck(Duck, XAngle, YAngle, ZAngle), |
| 44 | + % Project and draw the rotated duck |
| 45 | + project_and_draw(RotatedDuck, Renderer), |
| 46 | + % Present the rendered frame |
| 47 | + sdl_renderer:present(Renderer), |
| 48 | + % Process all events and determine if we should quit |
| 49 | + case handle_events() of |
| 50 | + quit -> ok; % Exit if quit event is detected |
| 51 | + continue -> |
| 52 | + % Sleep for ~16ms (approx 60 FPS) and continue with updated angles |
| 53 | + timer:sleep(16), |
| 54 | + loop(Window, Renderer, Duck, {XAngle + 0.03, YAngle + 0.05, ZAngle + 0.04}) |
| 55 | + end. |
| 56 | + |
| 57 | +% Handle all pending events in the queue |
| 58 | +handle_events() -> |
| 59 | + case sdl_events:poll() of |
| 60 | + #{type := quit} -> quit; % Quit event detected |
| 61 | + #{type := _} -> handle_events(); % Other event, keep processing |
| 62 | + false -> continue % No more events, proceed with rendering |
| 63 | + end. |
| 64 | + |
| 65 | +% Rotate duck vertices around X, Y, and Z axes |
| 66 | +rotate_duck(Vertices, XAngle, YAngle, ZAngle) -> |
| 67 | + RotX = matrix3d:rotate_x(XAngle), |
| 68 | + RotY = matrix3d:rotate_y(YAngle), |
| 69 | + RotZ = matrix3d:rotate_z(ZAngle), |
| 70 | + % Combine rotation matrices: X * (Y * Z) |
| 71 | + TempMatrix = matrix3d:multiply(RotY, RotZ), |
| 72 | + CombinedMatrix = matrix3d:multiply(RotX, TempMatrix), |
| 73 | + [matrix3d:apply(CombinedMatrix, Vertex) || Vertex <- Vertices]. |
| 74 | + |
| 75 | +% Define duck vertices (unchanged from your description) |
| 76 | +duck_vertices() -> |
| 77 | + [ |
| 78 | + % Body: 0-11 |
| 79 | + {1.0, 1.4, 0.8}, {1.0, 1.4, -0.8}, {1.4, 0.6, 0.8}, {1.4, 0.6, -0.8}, |
| 80 | + {1.0, -1.0, 0.8}, {1.0, -1.0, -0.8}, {-1.0, 1.4, 0.8}, {-1.0, 1.4, -0.8}, |
| 81 | + {-1.4, 0.6, 0.8}, {-1.4, 0.6, -0.8}, {-1.0, -1.0, 0.8}, {-1.0, -1.0, -0.8}, |
| 82 | + % Head: 12-19 |
| 83 | + {-1.4, 2.0, 0.6}, {-1.4, 2.0, -0.6}, {-1.4, 1.2, 0.6}, {-1.4, 1.2, -0.6}, |
| 84 | + {-2.0, 2.0, 0.6}, {-2.0, 2.0, -0.6}, {-2.0, 1.2, 0.6}, {-2.0, 1.2, -0.6}, |
| 85 | + % Beak: 20-25 |
| 86 | + {-2.4, 1.8, 0.2}, {-2.4, 1.8, -0.2}, {-2.4, 1.4, 0.2}, {-2.4, 1.4, -0.2}, |
| 87 | + {-2.0, 1.6, 0.2}, {-2.0, 1.6, -0.2}, |
| 88 | + % Right Leg: 26-29 |
| 89 | + {-1.0, -1.0, -0.3}, {-1.0, -2.0, -0.3}, {-1.2, -2.0, -0.1}, {-1.2, -2.0, -0.5}, |
| 90 | + % Left Leg: 30-33 |
| 91 | + {-1.0, -1.0, 0.3}, {-1.0, -2.0, 0.3}, {-1.2, -2.0, 0.1}, {-1.2, -2.0, 0.5} |
| 92 | + ]. |
| 93 | + |
| 94 | +% Project 3D vertices to 2D and draw filled triangles |
| 95 | +project_and_draw(Vertices, Renderer) -> |
| 96 | + Projected = [project_vertex(V) || V <- Vertices], |
| 97 | + |
| 98 | + % Body - Front (bright yellow) |
| 99 | + sdl_renderer:set_draw_color(Renderer, 255, 255, 0, 255), |
| 100 | + FrontTriangles = [ |
| 101 | + [0, 2, 4], [6, 8, 10], [0, 6, 10], [0, 4, 10] |
| 102 | + ], |
| 103 | + [fill_triangle(Renderer, [lists:nth(A+1, Projected) || A <- Tri]) || Tri <- FrontTriangles], |
| 104 | + |
| 105 | + % Body - Back (darker yellow) |
| 106 | + sdl_renderer:set_draw_color(Renderer, 220, 220, 0, 255), |
| 107 | + BackTriangles = [ |
| 108 | + [1, 3, 5], [7, 9, 11], [1, 7, 11], [1, 5, 11] |
| 109 | + ], |
| 110 | + [fill_triangle(Renderer, [lists:nth(A+1, Projected) || A <- Tri]) || Tri <- BackTriangles], |
| 111 | + |
| 112 | + % Body - Right (golden yellow) |
| 113 | + sdl_renderer:set_draw_color(Renderer, 255, 215, 0, 255), |
| 114 | + RightTriangles = [ |
| 115 | + [0, 1, 3], [3, 5, 4], [0, 2, 4] |
| 116 | + ], |
| 117 | + [fill_triangle(Renderer, [lists:nth(A+1, Projected) || A <- Tri]) || Tri <- RightTriangles], |
| 118 | + |
| 119 | + % Body - Left (pale yellow) |
| 120 | + sdl_renderer:set_draw_color(Renderer, 255, 245, 0, 255), |
| 121 | + LeftTriangles = [ |
| 122 | + [6, 7, 9], [9, 11, 10], [6, 8, 10] |
| 123 | + ], |
| 124 | + [fill_triangle(Renderer, [lists:nth(A+1, Projected) || A <- Tri]) || Tri <- LeftTriangles], |
| 125 | + |
| 126 | + % Body - Top (light yellow) |
| 127 | + sdl_renderer:set_draw_color(Renderer, 255, 255, 100, 255), |
| 128 | + TopTriangles = [ |
| 129 | + [0, 1, 6], [1, 7, 6] |
| 130 | + ], |
| 131 | + [fill_triangle(Renderer, [lists:nth(A+1, Projected) || A <- Tri]) || Tri <- TopTriangles], |
| 132 | + |
| 133 | + % Body - Bottom (deep yellow) |
| 134 | + sdl_renderer:set_draw_color(Renderer, 255, 200, 0, 255), |
| 135 | + BottomTriangles = [ |
| 136 | + [4, 5, 10], [5, 11, 10] |
| 137 | + ], |
| 138 | + [fill_triangle(Renderer, [lists:nth(A+1, Projected) || A <- Tri]) || Tri <- BottomTriangles], |
| 139 | + |
| 140 | + % Neck (match head color: slightly greenish yellow) |
| 141 | + sdl_renderer:set_draw_color(Renderer, 245, 255, 0, 255), |
| 142 | + NeckTriangles = [ |
| 143 | + [6, 7, 18], [7, 19, 18], % Left-back to head bottom |
| 144 | + [0, 1, 14], [1, 15, 14] % Right-front to head bottom |
| 145 | + ], |
| 146 | + [fill_triangle(Renderer, [lists:nth(A+1, Projected) || A <- Tri]) || Tri <- NeckTriangles], |
| 147 | + |
| 148 | + % Head (slightly greenish yellow) |
| 149 | + sdl_renderer:set_draw_color(Renderer, 245, 255, 0, 255), |
| 150 | + HeadTriangles = [ |
| 151 | + [12, 13, 15], [12, 14, 15], [16, 17, 19], [16, 18, 19], |
| 152 | + [12, 16, 18], [12, 14, 18], [13, 17, 19], [13, 15, 19] |
| 153 | + ], |
| 154 | + [fill_triangle(Renderer, [lists:nth(A+1, Projected) || A <- Tri]) || Tri <- HeadTriangles], |
| 155 | + |
| 156 | + % Beak (orange-yellow) |
| 157 | + sdl_renderer:set_draw_color(Renderer, 255, 235, 0, 255), |
| 158 | + BeakTriangles = [ |
| 159 | + [20, 21, 23], [20, 22, 23], [20, 24, 25], [20, 21, 25], |
| 160 | + [22, 24, 25], [22, 23, 25] |
| 161 | + ], |
| 162 | + [fill_triangle(Renderer, [lists:nth(A+1, Projected) || A <- Tri]) || Tri <- BeakTriangles], |
| 163 | + |
| 164 | + % Legs (muted yellow) |
| 165 | + sdl_renderer:set_draw_color(Renderer, 235, 235, 0, 255), |
| 166 | + LegTriangles = [ |
| 167 | + [26, 27, 28], [26, 27, 29], [30, 31, 32], [30, 31, 33] |
| 168 | + ], |
| 169 | + [fill_triangle(Renderer, [lists:nth(A+1, Projected) || A <- Tri]) || Tri <- LegTriangles], |
| 170 | + |
| 171 | + % Draw edges in black |
| 172 | + sdl_renderer:set_draw_color(Renderer, 0, 0, 0, 255), |
| 173 | + Edges = [ |
| 174 | + {0, 1}, {1, 3}, {3, 5}, {5, 4}, {4, 2}, {2, 0}, |
| 175 | + {6, 7}, {7, 9}, {9, 11}, {11, 10}, {10, 8}, {8, 6}, |
| 176 | + {0, 6}, {1, 7}, {2, 8}, {3, 9}, {4, 10}, {5, 11}, |
| 177 | + {12, 13}, {13, 15}, {15, 14}, {14, 12}, |
| 178 | + {16, 17}, {17, 19}, {19, 18}, {18, 16}, |
| 179 | + {12, 16}, {13, 17}, {14, 18}, {15, 19}, |
| 180 | + {6, 16}, {7, 17}, {8, 18}, {9, 19}, |
| 181 | + {20, 21}, {21, 23}, {23, 22}, {22, 20}, |
| 182 | + {20, 24}, {21, 25}, {22, 24}, {23, 25}, {24, 25}, |
| 183 | + {16, 24}, {17, 25}, |
| 184 | + {26, 27}, {27, 28}, {27, 29}, |
| 185 | + {30, 31}, {31, 32}, {31, 33} |
| 186 | + ], |
| 187 | + [draw_line(Renderer, lists:nth(A+1, Projected), lists:nth(B+1, Projected)) || {A, B} <- Edges]. |
| 188 | + |
| 189 | +% Project a 3D vertex to 2D screen coordinates (unchanged) |
| 190 | +project_vertex({X, Y, Z}) -> |
| 191 | + ZOffset = Z + 10, |
| 192 | + Scale = 800, |
| 193 | + ScreenX = 400 + round((X / ZOffset) * Scale), |
| 194 | + ScreenY = 300 + round((Y / ZOffset) * Scale), |
| 195 | + {ScreenX, ScreenY}. |
| 196 | + |
| 197 | +% Draw a line (unchanged) |
| 198 | +draw_line(Renderer, {X1, Y1}, {X2, Y2}) -> |
| 199 | + sdl_renderer:draw_line(Renderer, X1, Y1, X2, Y2). |
| 200 | + |
| 201 | +% Fill a triangle with scanline approach, handling degenerate cases |
| 202 | +fill_triangle(Renderer, Points) -> |
| 203 | + [{X1, Y1}, {X2, Y2}, {X3, Y3}] = lists:sort(fun({_, YA}, {_, YB}) -> YA =< YB end, Points), |
| 204 | + if |
| 205 | + Y1 == Y3 -> % Degenerate case: all Ys equal |
| 206 | + if |
| 207 | + X1 == X2 andalso X2 == X3 -> ok; % Single point |
| 208 | + true -> |
| 209 | + XMin = lists:min([X1, X2, X3]), |
| 210 | + XMax = lists:max([X1, X2, X3]), |
| 211 | + sdl_renderer:draw_line(Renderer, XMin, Y1, XMax, Y1) |
| 212 | + end; |
| 213 | + true -> |
| 214 | + case {Y1 == Y2, Y2 == Y3} of |
| 215 | + {true, false} -> % Flat top |
| 216 | + fill_flat_top_triangle(Renderer, X1, Y1, X2, Y2, X3, Y3); |
| 217 | + {false, true} -> % Flat bottom |
| 218 | + fill_flat_bottom_triangle(Renderer, X1, Y1, X2, Y2, X3, Y3); |
| 219 | + {false, false} -> % General case |
| 220 | + T = (Y2 - Y1) / (Y3 - Y1), |
| 221 | + XMid = X1 + round(T * (X3 - X1)), |
| 222 | + fill_flat_bottom_triangle(Renderer, X1, Y1, X2, Y2, XMid, Y2), |
| 223 | + fill_flat_top_triangle(Renderer, X2, Y2, XMid, Y2, X3, Y3) |
| 224 | + end |
| 225 | + end. |
| 226 | + |
| 227 | +% Fill a flat-bottom triangle |
| 228 | +fill_flat_bottom_triangle(Renderer, X1, Y1, X2, Y2, X3, _) -> |
| 229 | + lists:foreach( |
| 230 | + fun(Y) -> |
| 231 | + T = (Y - Y1) / (Y2 - Y1), |
| 232 | + XA = X1 + round(T * (X2 - X1)), |
| 233 | + XB = X1 + round(T * (X3 - X1)), |
| 234 | + {XStart, XEnd} = if XA =< XB -> {XA, XB}; true -> {XB, XA} end, |
| 235 | + sdl_renderer:draw_line(Renderer, XStart, Y, XEnd, Y) |
| 236 | + end, |
| 237 | + lists:seq(round(Y1), round(Y2)) |
| 238 | + ). |
| 239 | + |
| 240 | +% Fill a flat-top triangle |
| 241 | +fill_flat_top_triangle(Renderer, X1, Y1, X2, _, X3, Y3) -> |
| 242 | + lists:foreach( |
| 243 | + fun(Y) -> |
| 244 | + T = (Y - Y1) / (Y3 - Y1), |
| 245 | + XA = X1 + round(T * (X3 - X1)), |
| 246 | + XB = X2 + round(T * (X3 - X2)), |
| 247 | + {XStart, XEnd} = if XA =< XB -> {XA, XB}; true -> {XB, XA} end, |
| 248 | + sdl_renderer:draw_line(Renderer, XStart, Y, XEnd, Y) |
| 249 | + end, |
| 250 | + lists:seq(round(Y1), round(Y3)) |
| 251 | + ). |
0 commit comments