diff options
| -rw-r--r-- | Makefile | 6 | ||||
| -rw-r--r-- | examples/duck_engine/duck_engine.erl | 251 | ||||
| -rw-r--r-- | examples/duck_engine/matrix3d.erl | 50 | ||||
| -rwxr-xr-x | examples/duck_engine/start.sh | 2 | 
4 files changed, 308 insertions, 1 deletions
| @@ -31,7 +31,7 @@ include erlang.mk  SDL2_CFLAGS = $(shell sdl2-config --cflags)  CFLAGS += $(SDL2_CFLAGS)  # @todo -undefined dynamic_lookup on OSX? -LDLIBS += -lSDL2 -lSDL2_image -lSDL2_ttf +LDLIBS += $(shell sdl2-config --libs) -lSDL2_image -lSDL2_ttf  # Clean the environment before each CI builds. @@ -56,3 +56,7 @@ hello_sdl:: all  bullet_engine:: all  	erlc -o examples/bullet_engine examples/bullet_engine/*.erl  	cd examples/bullet_engine && ./start.sh + +duck_engine: all +	erlc -o examples/duck_engine examples/duck_engine/*.erl +	cd examples/duck_engine && ./start.sh diff --git a/examples/duck_engine/duck_engine.erl b/examples/duck_engine/duck_engine.erl new file mode 100644 index 0000000..ab895c0 --- /dev/null +++ b/examples/duck_engine/duck_engine.erl @@ -0,0 +1,251 @@ +%% This is an example. Feel free to copy and reuse as you wish. +%% +%% This example was almost entirely generated by Grok 3 with +%% minor human-driven changes (such as fixing the position of +%% legs). The raster is imperfect, either missing sides or +%% sides being incorrectly defined. +%% +%% The conversation leading to this example can be found at +%% https://x.com/i/grok/share/IN1mWW5a4g1zcqYTInA4X8BXP + +-module(duck_engine). +-export([run/0]). + +run() -> +    % Initialize SDL with video subsystem +    case sdl:start([video]) of +        ok -> +            % Ensure SDL stops when this process exits +            sdl:stop_on_exit(), +            % Create a window (title, x, y, width, height, flags) +            {ok, Window} = sdl_window:create(<<"Duck Engine">>, 100, 100, 800, 600, []), +            % Create a renderer with acceleration and vsync +            {ok, Renderer} = sdl_renderer:create(Window, -1, [accelerated, present_vsync]), +            % Define initial duck vertices +            Duck = duck_vertices(), +            % Start the rendering loop with initial rotation angles +            loop(Window, Renderer, Duck, {0.0, 0.0, 0.0}); +        {application_start_error, Reason} -> +            io:format("Failed to start esdl2 application: ~p~n", [Reason]), +            error; +        {error, String} -> +            io:format("SDL initialization failed: ~s~n", [String]), +            error +    end. + +% Main rendering loop +loop(Window, Renderer, Duck, {XAngle, YAngle, ZAngle}) -> +    % Set clear color to white (R, G, B, A) +    sdl_renderer:set_draw_color(Renderer, 255, 255, 255, 255), +    % Clear the renderer +    sdl_renderer:clear(Renderer), +    % Rotate the duck vertices +    RotatedDuck = rotate_duck(Duck, XAngle, YAngle, ZAngle), +    % Project and draw the rotated duck +    project_and_draw(RotatedDuck, Renderer), +    % Present the rendered frame +    sdl_renderer:present(Renderer), +    % Process all events and determine if we should quit +    case handle_events() of +        quit -> ok; % Exit if quit event is detected +        continue -> +            % Sleep for ~16ms (approx 60 FPS) and continue with updated angles +            timer:sleep(16), +            loop(Window, Renderer, Duck, {XAngle + 0.03, YAngle + 0.05, ZAngle + 0.04}) +    end. + +% Handle all pending events in the queue +handle_events() -> +    case sdl_events:poll() of +        #{type := quit} -> quit; % Quit event detected +        #{type := _} -> handle_events(); % Other event, keep processing +        false -> continue % No more events, proceed with rendering +    end. + +% Rotate duck vertices around X, Y, and Z axes +rotate_duck(Vertices, XAngle, YAngle, ZAngle) -> +    RotX = matrix3d:rotate_x(XAngle), +    RotY = matrix3d:rotate_y(YAngle), +    RotZ = matrix3d:rotate_z(ZAngle), +    % Combine rotation matrices: X * (Y * Z) +    TempMatrix = matrix3d:multiply(RotY, RotZ), +    CombinedMatrix = matrix3d:multiply(RotX, TempMatrix), +    [matrix3d:apply(CombinedMatrix, Vertex) || Vertex <- Vertices]. + +% Define duck vertices (unchanged from your description) +duck_vertices() -> +    [ +        % Body: 0-11 +        {1.0, 1.4, 0.8}, {1.0, 1.4, -0.8}, {1.4, 0.6, 0.8}, {1.4, 0.6, -0.8}, +        {1.0, -1.0, 0.8}, {1.0, -1.0, -0.8}, {-1.0, 1.4, 0.8}, {-1.0, 1.4, -0.8}, +        {-1.4, 0.6, 0.8}, {-1.4, 0.6, -0.8}, {-1.0, -1.0, 0.8}, {-1.0, -1.0, -0.8}, +        % Head: 12-19 +        {-1.4, 2.0, 0.6}, {-1.4, 2.0, -0.6}, {-1.4, 1.2, 0.6}, {-1.4, 1.2, -0.6}, +        {-2.0, 2.0, 0.6}, {-2.0, 2.0, -0.6}, {-2.0, 1.2, 0.6}, {-2.0, 1.2, -0.6}, +        % Beak: 20-25 +        {-2.4, 1.8, 0.2}, {-2.4, 1.8, -0.2}, {-2.4, 1.4, 0.2}, {-2.4, 1.4, -0.2}, +        {-2.0, 1.6, 0.2}, {-2.0, 1.6, -0.2}, +        % Right Leg: 26-29 +        {-1.0, -1.0, -0.3}, {-1.0, -2.0, -0.3}, {-1.2, -2.0, -0.1}, {-1.2, -2.0, -0.5}, +        % Left Leg: 30-33 +        {-1.0, -1.0, 0.3}, {-1.0, -2.0, 0.3}, {-1.2, -2.0, 0.1}, {-1.2, -2.0, 0.5} +    ]. + +% Project 3D vertices to 2D and draw filled triangles +project_and_draw(Vertices, Renderer) -> +    Projected = [project_vertex(V) || V <- Vertices], + +    % Body - Front (bright yellow) +    sdl_renderer:set_draw_color(Renderer, 255, 255, 0, 255), +    FrontTriangles = [ +        [0, 2, 4], [6, 8, 10], [0, 6, 10], [0, 4, 10] +    ], +    [fill_triangle(Renderer, [lists:nth(A+1, Projected) || A <- Tri]) || Tri <- FrontTriangles], + +    % Body - Back (darker yellow) +    sdl_renderer:set_draw_color(Renderer, 220, 220, 0, 255), +    BackTriangles = [ +        [1, 3, 5], [7, 9, 11], [1, 7, 11], [1, 5, 11] +    ], +    [fill_triangle(Renderer, [lists:nth(A+1, Projected) || A <- Tri]) || Tri <- BackTriangles], + +    % Body - Right (golden yellow) +    sdl_renderer:set_draw_color(Renderer, 255, 215, 0, 255), +    RightTriangles = [ +        [0, 1, 3], [3, 5, 4], [0, 2, 4] +    ], +    [fill_triangle(Renderer, [lists:nth(A+1, Projected) || A <- Tri]) || Tri <- RightTriangles], + +    % Body - Left (pale yellow) +    sdl_renderer:set_draw_color(Renderer, 255, 245, 0, 255), +    LeftTriangles = [ +        [6, 7, 9], [9, 11, 10], [6, 8, 10] +    ], +    [fill_triangle(Renderer, [lists:nth(A+1, Projected) || A <- Tri]) || Tri <- LeftTriangles], + +    % Body - Top (light yellow) +    sdl_renderer:set_draw_color(Renderer, 255, 255, 100, 255), +    TopTriangles = [ +        [0, 1, 6], [1, 7, 6] +    ], +    [fill_triangle(Renderer, [lists:nth(A+1, Projected) || A <- Tri]) || Tri <- TopTriangles], + +    % Body - Bottom (deep yellow) +    sdl_renderer:set_draw_color(Renderer, 255, 200, 0, 255), +    BottomTriangles = [ +        [4, 5, 10], [5, 11, 10] +    ], +    [fill_triangle(Renderer, [lists:nth(A+1, Projected) || A <- Tri]) || Tri <- BottomTriangles], + +    % Neck (match head color: slightly greenish yellow) +    sdl_renderer:set_draw_color(Renderer, 245, 255, 0, 255), +    NeckTriangles = [ +        [6, 7, 18], [7, 19, 18],  % Left-back to head bottom +        [0, 1, 14], [1, 15, 14]   % Right-front to head bottom +    ], +    [fill_triangle(Renderer, [lists:nth(A+1, Projected) || A <- Tri]) || Tri <- NeckTriangles], + +    % Head (slightly greenish yellow) +    sdl_renderer:set_draw_color(Renderer, 245, 255, 0, 255), +    HeadTriangles = [ +        [12, 13, 15], [12, 14, 15], [16, 17, 19], [16, 18, 19], +        [12, 16, 18], [12, 14, 18], [13, 17, 19], [13, 15, 19] +    ], +    [fill_triangle(Renderer, [lists:nth(A+1, Projected) || A <- Tri]) || Tri <- HeadTriangles], + +    % Beak (orange-yellow) +    sdl_renderer:set_draw_color(Renderer, 255, 235, 0, 255), +    BeakTriangles = [ +        [20, 21, 23], [20, 22, 23], [20, 24, 25], [20, 21, 25], +        [22, 24, 25], [22, 23, 25] +    ], +    [fill_triangle(Renderer, [lists:nth(A+1, Projected) || A <- Tri]) || Tri <- BeakTriangles], + +    % Legs (muted yellow) +    sdl_renderer:set_draw_color(Renderer, 235, 235, 0, 255), +    LegTriangles = [ +        [26, 27, 28], [26, 27, 29], [30, 31, 32], [30, 31, 33] +    ], +    [fill_triangle(Renderer, [lists:nth(A+1, Projected) || A <- Tri]) || Tri <- LegTriangles], + +    % Draw edges in black +    sdl_renderer:set_draw_color(Renderer, 0, 0, 0, 255), +    Edges = [ +        {0, 1}, {1, 3}, {3, 5}, {5, 4}, {4, 2}, {2, 0}, +        {6, 7}, {7, 9}, {9, 11}, {11, 10}, {10, 8}, {8, 6}, +        {0, 6}, {1, 7}, {2, 8}, {3, 9}, {4, 10}, {5, 11}, +        {12, 13}, {13, 15}, {15, 14}, {14, 12}, +        {16, 17}, {17, 19}, {19, 18}, {18, 16}, +        {12, 16}, {13, 17}, {14, 18}, {15, 19}, +        {6, 16}, {7, 17}, {8, 18}, {9, 19}, +        {20, 21}, {21, 23}, {23, 22}, {22, 20}, +        {20, 24}, {21, 25}, {22, 24}, {23, 25}, {24, 25}, +        {16, 24}, {17, 25}, +        {26, 27}, {27, 28}, {27, 29}, +        {30, 31}, {31, 32}, {31, 33} +    ], +    [draw_line(Renderer, lists:nth(A+1, Projected), lists:nth(B+1, Projected)) || {A, B} <- Edges]. + +% Project a 3D vertex to 2D screen coordinates (unchanged) +project_vertex({X, Y, Z}) -> +    ZOffset = Z + 10, +    Scale = 800, +    ScreenX = 400 + round((X / ZOffset) * Scale), +    ScreenY = 300 + round((Y / ZOffset) * Scale), +    {ScreenX, ScreenY}. + +% Draw a line (unchanged) +draw_line(Renderer, {X1, Y1}, {X2, Y2}) -> +    sdl_renderer:draw_line(Renderer, X1, Y1, X2, Y2). + +% Fill a triangle with scanline approach, handling degenerate cases +fill_triangle(Renderer, Points) -> +    [{X1, Y1}, {X2, Y2}, {X3, Y3}] = lists:sort(fun({_, YA}, {_, YB}) -> YA =< YB end, Points), +    if +        Y1 == Y3 -> % Degenerate case: all Ys equal +            if +                X1 == X2 andalso X2 == X3 -> ok; % Single point +                true -> +                    XMin = lists:min([X1, X2, X3]), +                    XMax = lists:max([X1, X2, X3]), +                    sdl_renderer:draw_line(Renderer, XMin, Y1, XMax, Y1) +            end; +        true -> +            case {Y1 == Y2, Y2 == Y3} of +                {true, false} -> % Flat top +                    fill_flat_top_triangle(Renderer, X1, Y1, X2, Y2, X3, Y3); +                {false, true} -> % Flat bottom +                    fill_flat_bottom_triangle(Renderer, X1, Y1, X2, Y2, X3, Y3); +                {false, false} -> % General case +                    T = (Y2 - Y1) / (Y3 - Y1), +                    XMid = X1 + round(T * (X3 - X1)), +                    fill_flat_bottom_triangle(Renderer, X1, Y1, X2, Y2, XMid, Y2), +                    fill_flat_top_triangle(Renderer, X2, Y2, XMid, Y2, X3, Y3) +            end +    end. + +% Fill a flat-bottom triangle +fill_flat_bottom_triangle(Renderer, X1, Y1, X2, Y2, X3, _) -> +    lists:foreach( +        fun(Y) -> +            T = (Y - Y1) / (Y2 - Y1), +            XA = X1 + round(T * (X2 - X1)), +            XB = X1 + round(T * (X3 - X1)), +            {XStart, XEnd} = if XA =< XB -> {XA, XB}; true -> {XB, XA} end, +            sdl_renderer:draw_line(Renderer, XStart, Y, XEnd, Y) +        end, +        lists:seq(round(Y1), round(Y2)) +    ). + +% Fill a flat-top triangle +fill_flat_top_triangle(Renderer, X1, Y1, X2, _, X3, Y3) -> +    lists:foreach( +        fun(Y) -> +            T = (Y - Y1) / (Y3 - Y1), +            XA = X1 + round(T * (X3 - X1)), +            XB = X2 + round(T * (X3 - X2)), +            {XStart, XEnd} = if XA =< XB -> {XA, XB}; true -> {XB, XA} end, +            sdl_renderer:draw_line(Renderer, XStart, Y, XEnd, Y) +        end, +        lists:seq(round(Y1), round(Y3)) +    ). diff --git a/examples/duck_engine/matrix3d.erl b/examples/duck_engine/matrix3d.erl new file mode 100644 index 0000000..5846be2 --- /dev/null +++ b/examples/duck_engine/matrix3d.erl @@ -0,0 +1,50 @@ +%% This is an example. Feel free to copy and reuse as you wish. + +-module(matrix3d). +-export([rotate_x/1, rotate_y/1, rotate_z/1, multiply/2, apply/2]). + +% Rotation matrix around X-axis (angle in radians) +rotate_x(Angle) -> +    Cos = math:cos(Angle), +    Sin = math:sin(Angle), +    [[1, 0,    0   ], +     [0, Cos, -Sin ], +     [0, Sin,  Cos]]. + +% Rotation matrix around Y-axis +rotate_y(Angle) -> +    Cos = math:cos(Angle), +    Sin = math:sin(Angle), +    [[ Cos,  0, Sin ], +     [ 0,    1, 0   ], +     [-Sin,  0, Cos]]. + +% Rotation matrix around Z-axis +rotate_z(Angle) -> +    Cos = math:cos(Angle), +    Sin = math:sin(Angle), +    [[Cos, -Sin, 0 ], +     [Sin,  Cos, 0 ], +     [0,    0,   1]]. + +% Multiply two 3x3 matrices +multiply(A, B) -> +    [[A11, A12, A13], +     [A21, A22, A23], +     [A31, A32, A33]] = A, +    [[B11, B12, B13], +     [B21, B22, B23], +     [B31, B32, B33]] = B, +    [[A11*B11 + A12*B21 + A13*B31, A11*B12 + A12*B22 + A13*B32, A11*B13 + A12*B23 + A13*B33], +     [A21*B11 + A22*B21 + A23*B31, A21*B12 + A22*B22 + A23*B32, A21*B13 + A22*B23 + A23*B33], +     [A31*B11 + A32*B21 + A33*B31, A31*B12 + A32*B22 + A33*B32, A31*B13 + A32*B23 + A33*B33]]. + +% Apply a 3x3 matrix to a 3D vertex {X, Y, Z} +apply(Matrix, {X, Y, Z}) -> +    [[M11, M12, M13], +     [M21, M22, M23], +     [M31, M32, M33]] = Matrix, +    NewX = M11 * X + M12 * Y + M13 * Z, +    NewY = M21 * X + M22 * Y + M23 * Z, +    NewZ = M31 * X + M32 * Y + M33 * Z, +    {NewX, NewY, NewZ}. diff --git a/examples/duck_engine/start.sh b/examples/duck_engine/start.sh new file mode 100755 index 0000000..7b24eca --- /dev/null +++ b/examples/duck_engine/start.sh @@ -0,0 +1,2 @@ +#!/bin/sh +erl -smp enable +stbt db -pa ../../ebin -eval "duck_engine:run()." | 
