A downloadable asset pack

Download NowName your own price

Welcome to another tutorial, in this one I am going to show you how to transform any tilemap layer of a room into a cylindrical model. (Make sure to download the project files so you can follow along)

(Tutorial 2 can be found here)

Part 1: Setting up Vertex Format

Create a new script called build_vertex_format

function build_vertex_format(){
    gml_pragma("global", "build_vertex_format()");
    
    /// start the vertex format
    vertex_format_begin();
    /// add x,y,z
    vertex_format_add_position_3d();
    /// add nx, ny, nz
    vertex_format_add_normal();
    /// add u, v
    vertex_format_add_texcoord();
    /// add color, alpha
    vertex_format_add_colour();
    
    global._vformat_ = vertex_format_end();
}

This piece of code defines a function called `build_vertex_format()` that is intended to set up a specific format for vertices in a 3D graphics rendering context for GameMaker.

Here's a breakdown of what each part of the code does:

1. `gml_pragma("global", "build_vertex_format()");`: This will call the script function "build_vertex_format" before the first room of the game is run.

2. `vertex_format_begin();`: This function call starts the definition of a vertex format. A vertex format is a specification that defines the layout of information associated with a single vertex in a 3D model, such as its position, normal, texture coordinates, color, etc.

3. `vertex_format_add_position_3d();`: This function call indicates that the vertex format will include a 3D position (x, y, z coordinates) for each vertex. This information is necessary for placing the vertex in 3D space.

4. `vertex_format_add_normal();`: This function call adds a normal vector (nx, ny, nz) to the vertex format. Normals are used to determine how light interacts with a surface, influencing shading and reflections.

5. `vertex_format_add_texcoord();`: This function call adds texture coordinates (u, v) to the vertex format. Texture coordinates determine how a texture is mapped onto the surface of a 3D model.

6. `vertex_format_add_colour();`: This function call adds color and alpha (transparency) information to the vertex format. Color affects how the vertex is visually rendered, and the alpha value controls its transparency.

7. `global._vformat_ = vertex_format_end();`: This line ends the vertex format definition and stores the resulting format information in a global variable called `_vformat_`. This variable is likely to be used later when creating and rendering 3D objects with the specified vertex format.

Overall, this code snippet sets up the specifications for how individual vertices will store and present information like position, normals, texture coordinates, color, and transparency in a 3D environment, likely for creating a cylindrical world similar to the style seen in Animal Crossing. The vertex format established here will be used when constructing and rendering 3D objects within the game or application.

Part 2: Vertices!

We will need to simplify the wall of text that Game Makers built in vertex defining functions will vomit on us, so lets create a function called "vertex_add_point"

function vertex_add_point(vbuff, xx, yy ,zz, nx, ny, nz, ux, uy, col, alpha){
    vertex_position_3d(vbuff, xx, yy, zz);
    vertex_normal(vbuff, nx, ny, nz);
    vertex_texcoord(vbuff, ux, uy);
    vertex_color(vbuff, col, alpha);
}

This function, `vertex_add_point`, Is used for adding information about a single vertex to a buffer that holds the data for rendering 3D graphics. Each vertex represents a point in 3D space and contains various attributes like position, normal, texture coordinates, color, and alpha (transparency). This function simplifies the process of adding these attributes to the buffer by providing a clear way to input all the necessary information.

Here's a breakdown of what each parameter of the `vertex_add_point` function does:

1. `vbuff`: This parameter represents the vertex buffer to which the vertex information is being added. A vertex buffer is a data structure that holds a collection of vertices that define the shape and appearance of a 3D object.

2. `xx`, `yy`, `zz`: These parameters represent the X, Y, and Z coordinates of the vertex's position in 3D space. They define where the vertex is located.

3. `nx`, `ny`, `nz`: These parameters represent the X, Y, and Z components of the vertex's normal vector. The normal vector defines the direction perpendicular to the surface at that vertex. This information is crucial for lighting and shading calculations.

4. `ux`, `uy`: These parameters represent the U and V texture coordinates of the vertex. These coordinates determine how the texture is mapped onto the vertex's surface.

5. `col`: This parameter represents the color of the vertex. It could be specified as an RGB (red, green, blue) color value.

6. `alpha`: This parameter represents the transparency or opacity of the vertex. It usually ranges from 0 (fully transparent) to 1 (fully opaque).

In essence, this function combines the various pieces of information required for a vertex into a single call, making it easier to add vertices to a vertex buffer. The function helps streamline the process of defining and creating the vertices that ultimately form 3D objects in a graphics rendering context.

Part 3: Building A Cylinder!

Now we have the building blocks for generating a 3D cylinder, lets create a new function called "tilemap_get_vertex_buffer_cylinder"


function tilemap_get_vertex_buffer_cylinder(layer_name, radius = 200, vertex_format = global._vformat_) {
    // Get the ID of the tilemap layer.
    var tilemap = layer_get_id(layer_name);
    // Get the ID of the tileset associated with the tilemap.
    var tm_tileset = tilemap_get_tileset(tilemap);
    // Get information about the tileset.
    var ts_info = tileset_get_info(tm_tileset);
    var ts_tile_width = ts_info.tile_width;
    var ts_tile_height = ts_info.tile_height;
    var ts_horizontal_count = ts_info.tile_columns;
    // Get the texture coordinates (UVs) of the tileset.
    var ts_uvs = tileset_get_uvs(tm_tileset);
    var ts_uvs_left = ts_uvs[0];
    var ts_uvs_top = ts_uvs[1];
    var ts_uvs_right = ts_uvs[2];
    var ts_uvs_bottom = ts_uvs[3];
    // Get the texture information of the tileset.
    var ts_texture = tileset_get_texture(tm_tileset);
    var ts_texel_width = texture_get_texel_width(ts_texture);
    var ts_texel_height = texture_get_texel_height(ts_texture);
    var ts_texel_tile_width = ts_texel_width * ts_tile_width;
    var ts_texel_tile_height = ts_texel_height * ts_tile_height;
    // Get the dimensions of the tilemap.
    var tm_width = tilemap_get_width(tilemap);
    var tm_height = tilemap_get_height(tilemap);
    // Create a vertex buffer.
    var vbuff = vertex_create_buffer();
    
    // Begin adding vertices to the buffer using the specified format.
    vertex_begin(vbuff, vertex_format);
    
    /// for cylinder mapping
    var delta_angle = 360 / tm_height;
    // Iterate through each tile in the tilemap.
    for (var i = 0; i < tm_width; ++i) {
        for (var j = 0; j < tm_height; ++j) {
            
            // calculate angles
            var angle1 = delta_angle * j + 180;
            var angle2 = delta_angle * (j+1) + 180;
            // calculate y/z position on cylinder
            var dy1 = lengthdir_x(radius, angle1);
            var dz1 = lengthdir_y(radius, angle1);
            var dy2 = lengthdir_x(radius, angle2);
            var dz2 = lengthdir_y(radius, angle2);
            
            // Get tile data at the current position.
            var tile_data = tilemap_get(tilemap, i, j);
            
            // Get the index of the tile within the tileset.
            var tile_index = tile_get_index(tile_data);
            // Calculate the tile's position within the tileset.
            var tile_index_x = tile_index mod ts_horizontal_count;
            var tile_index_y = tile_index div ts_horizontal_count;
            // Calculate UVs for the tile.
            var uv_left = ts_uvs_left + ts_texel_tile_width * tile_index_x;
            var uv_top = ts_uvs_top + ts_texel_tile_height * tile_index_y;
            var uv_right = uv_left + ts_texel_tile_width;
            var uv_bottom = uv_top + ts_texel_tile_height;
            // Calculate 3D points for the tile's vertices.
            var p1x = ts_tile_width * i;
            var p1y = dy1;
            var p1z = dz1;
            var p2x = p1x + ts_tile_width;
            var p2y = dy1;
            var p2z = dz1;
            var p3x = p1x + ts_tile_width;
            var p3y = dy2;
            var p3z = dz2;
            var p4x = p1x;
            var p4y = dy2;
            var p4z = dz2;
            // Calculate UVs relative to each point.
            var p1u = uv_left;
            var p1v = uv_top;
            var p2u = uv_right;
            var p2v = uv_top;
            var p3u = uv_right;
            var p3v = uv_bottom;
            var p4u = uv_left;
            var p4v = uv_bottom;
            
            /// Calculate normal - points in the same direction as y,z
            var nya = p1y/radius;
            var nza = p1z/radius;
            var nyb = p4y/radius;
            var nzb = p4z/radius;
            
            // Add vertices to the buffer for the tile's quads.
            vertex_add_point(vbuff, p1x, p1y, p1z, 0, nya, nza, p1u, p1v, c_white, 1);
            vertex_add_point(vbuff, p2x, p2y, p2z, 0, nya, nza, p2u, p2v, c_white, 1);
            vertex_add_point(vbuff, p3x, p3y, p3z, 0, nya, nza, p3u, p3v, c_white, 1);
            // Add vertices for the bottom half of the tile.
            vertex_add_point(vbuff, p3x, p3y, p3z, 0, nyb, nzb, p3u, p3v, c_white, 1);
            vertex_add_point(vbuff, p4x, p4y, p4z, 0, nyb, nzb, p4u, p4v, c_white, 1);
            vertex_add_point(vbuff, p1x, p1y, p1z, 0, nyb, nzb, p1u, p1v, c_white, 1);
        }
    }
    
    // End adding vertices to the buffer.
    vertex_end(vbuff);
    // Freeze the vertex buffer to optimize rendering.
    vertex_freeze(vbuff);
    // Set the tilemap layer as invisible.
    layer_set_visible(tilemap, false);
    // Return the created vertex buffer.
    return vbuff;
}

This massive and over-bloated function (thanks to the new tile system ;|) , named `tilemap_get_vertex_buffer_cylinder`, is used to generate a vertex buffer for rendering a cylindrical world using data from a tilemap in GameMaker. The cylindrical world is created by mapping the tiles from a tilemap onto the surface of a cylinder.

Here's a step-by-step explanation of what the function does:

1. Inputs:

   - `layer_name`: The name of the tilemap layer containing the tile data.

   - `radius` (default: 200): The radius of the cylinder's curvature.

   - `vertex_format` (default: `global._vformat_`): The vertex format specifying the attributes of each vertex.

2. Getting Tilemap and Tileset Information:

   - The function first gets the IDs of the tilemap and its associated tileset.

   - It gathers information about the tileset, such as tile width, height, and the number of tiles in a row (horizontal count).

   - It retrieves UV (texture coordinate) information for the tileset and its associated texture's dimensions.

3. Creating a Vertex Buffer:

   - A new vertex buffer is created using `vertex_create_buffer()`.

4. Starting Vertex Addition:

   - Vertex addition to the buffer is begun using `vertex_begin()` with the specified vertex format.

5. Cylinder Mapping:

   - The function calculates the angle increment (`delta_angle`) to distribute tiles around the cylindrical surface.

6. Iterating Through Tiles:

   - Two nested loops iterate through each tile in the tilemap.

   - For each tile:

     - Angles are calculated to position the tile's vertices on the cylinder's surface.

     - Y and Z coordinates for the vertices are determined using trigonometric functions (`lengthdir_x` and `lengthdir_y`).

7. Tile Information Retrieval:

   - The tile data at the current position is retrieved from the tilemap.

   - The index of the tile within the tileset is obtained.

8. Calculating UVs:

   - UV coordinates are calculated based on the tile's index, tileset's UVs, and texture dimensions.

9. Calculating Vertex Positions:

   - For each tile, the function calculates the 3D positions of the vertices of the tile's quad.

10. Calculating Normals:

    - Normals (ny, nz) are calculated for each vertex. The normals point in the direction of the Y and Z coordinates of each vertex.

11. Adding Vertices to Buffer:

    - Vertices for the top half of the tile's quad are added to the buffer using `vertex_add_point`.

    - Vertices for the bottom half of the tile are then added similarly.

12. Ending Vertex Addition:

    - Vertex addition is ended using `vertex_end()`.

13. Freezing Vertex Buffer:

    - The vertex buffer is frozen using `vertex_freeze()` to optimize rendering.

14. Setting Tilemap Layer as Invisible:

    - The tilemap layer is set as invisible using `layer_set_visible()` to prevent rendering duplicates.

15. Returning the Vertex Buffer:

    - The function returns the created vertex buffer for later rendering.

In summary, this function takes a tilemap, applies cylindrical mapping to the tiles, generates vertices with appropriate positions, normals, UVs, and colors, and creates a vertex buffer for rendering the cylindrical world. The cylindrical mapping technique creates the illusion of a 3D environment on a cylindrical surface by accurately positioning tiles around the cylinder's curvature. (Special thanks to DragoniteSpam for his tutorial on creating a flat plane from a tilemap)

Part 4: Draw It!

Now that we have a way to make a cylinder, your probably wondering... how do I draw it? Fear not, we just need a lot more code!

function setup_matrix(xx,yy,zz,rx,ry,rz,sx,sy,sz){
    var matrix_translate =    matrix_build(xx,yy,zz,    0,0,0,        1,1,1);
    var matrix_rotate =        matrix_build(0,0,0,        rx,ry,rz,    1,1,1);
    var matrix_scale =        matrix_build(0,0,0,        0,0,0,        sx,sy,sz);
    
    var matrix_rs = matrix_multiply(matrix_scale, matrix_rotate);
    var matrix_final = matrix_multiply(matrix_rs, matrix_translate);
    return(matrix_final);
}

This function is designed to create a transformation matrix that combines translation, rotation, and scaling operations. Transformation matrices are fundamental in computer graphics and game development as they allow you to manipulate the position, orientation, and size of objects in 3D space.

Here's a breakdown of what each part of the function does:

1. Inputs:

   - `xx`, `yy`, `zz`: The translation components along the X, Y, and Z axes.

   - `rx`, `ry`, `rz`: The rotation angles (in degrees) around the X, Y, and Z axes.

   - `sx`, `sy`, `sz`: The scaling factors along the X, Y, and Z axes.

2. Creating Translation, Rotation, and Scaling Matrices:

   - Three individual matrices are created:

     - `matrix_translate`: A translation matrix that shifts the object's position by `(xx, yy, zz)`.

     - `matrix_rotate`: A rotation matrix that rotates the object around its origin by the angles `(rx, ry, rz)` along the X, Y, and Z axes.

     - `matrix_scale`: A scaling matrix that scales the object by `(sx, sy, sz)` along the X, Y, and Z axes.

3. Combining Rotation and Scaling:

   - The matrices for rotation and scaling are combined into a single matrix called `matrix_rs` using matrix multiplication. This is done because applying rotation and scaling in a specific order can yield different results.

4. Final Transformation:

   - The final transformation matrix, `matrix_final`, is obtained by multiplying the `matrix_rs` with the `matrix_translate`. This ensures that translation, rotation, and scaling are all applied in the desired order.

5. Returning the Final Matrix:

   - The function returns the combined transformation matrix, which encapsulates the translation, rotation, and scaling operations specified by the input parameters.

In summary, the `setup_matrix` function provides a convenient way to create a transformation matrix that combines translation, rotation, and scaling operations. This matrix can then be used to transform vertices or objects in a 3D environment, enabling you to control their position, orientation, and size in a flexible and efficient manner.

Now, lets use it to draw the cylinder! Create the function "draw_tilemap_buffer" and put the following code:

function draw_tilemap_buffer(buff, tset, xx, yy, zz, angle){
    var tex = tileset_get_texture(tset);
    
    /// transform position
    matrix_set(matrix_world, setup_matrix(xx,yy,zz,angle,0,0,1,1,1));
    
    /// draw buffer
    vertex_submit(buff, pr_trianglelist, tex);
    
    /// reset matrix
    matrix_set(matrix_world, matrix_build_identity());
}

This function is designed to draw a tilemap that has been converted into a vertex buffer onto the screen using specified transformation parameters. It's a common practice in game development to create a vertex buffer for optimized rendering and then apply transformations before drawing the buffer.

Here's a breakdown of what each part of the function does:

1. Inputs:

   - `buff`: The vertex buffer containing the tilemap data in the desired format.

   - `tset`: The tileset associated with the tilemap.

   - `xx`, `yy`, `zz`: The translation components for the object's position in 3D space.

   - `angle`: The rotation angle (in degrees) around the object's origin.

2. Getting Tileset Texture:

   - The function retrieves the texture associated with the provided tileset using `tileset_get_texture(tset)`.

3. Transforming Position:

   - The function applies a transformation to the object's position using a transformation matrix. It does this by calling the previously explained `setup_matrix` function with the translation components, rotation angle, and scaling factors.

   - The transformation matrix is set as the world transformation matrix using `matrix_set(matrix_world, transformation_matrix)`.

4. Drawing the Buffer:

   - The vertex buffer containing the tilemap data is drawn to the screen using the `vertex_submit` function.

   - The function specifies that the submitted vertices form triangles (`pr_trianglelist`) and provides the texture (`tex`) that should be applied to the drawn triangles.

5. Resetting the Matrix:

   - After drawing the buffer, the world transformation matrix is reset to the identity matrix using `matrix_build_identity()`. This ensures that subsequent rendering operations are not affected by the previous transformations.

In summary, the `draw_tilemap_buffer` function takes a vertex buffer that represents a tilemap, applies a transformation to the buffer's position and orientation, and then draws the transformed buffer to the screen. This allows you to render the tilemap at a specific location, with a specific rotation, and potentially with other transformations applied. It's a powerful function that helps achieve efficient and visually appealing rendering of tile-based environments in a 3D space.

Then we just need to stuff the following in an object called obj_camera... In the create event put this:

tbuffer = tilemap_get_vertex_buffer_cylinder("tile_layer_name", room_height * 0.25);

Change the tile layer name to the layer you are using. Then, in the draw event, put this:

draw_tilemap_buffer(tbuffer, tileset_name, 0, room_height, 0, 0);

Make sure you add in the actual name of your tileset :)

Conclusion:

As we reach the conclusion of this tutorial series, you've embarked on an inspiring journey to bring life to your very own cylindrical world, reminiscent of the charming landscapes found in games like Animal Crossing. Throughout this series, you've peered deep into the abyss of GameMaker and 3D graphics, careful not to fall, to create a truly unique and immersive environment.

From the inception of the vertex format to the intricate details of cylindrical mapping, you've gained a comprehensive understanding of how to transform simple tiles into a breathtaking 3D realm. By combining elements such as position, normal vectors, textures, colors, and transparency, you've crafted a world that beckons players to explore its every nook and cranny, or maybe just around the leading edge ;).

The series started with the foundation of vertex formats, showcasing how essential it is to define the attributes that make up each vertex in your world. As you progressed, you learned to construct and manipulate vertex buffers, which serve as the building blocks of your 3D landscape. With these skills in your toolkit, you advanced into the realm of cylindrical mapping, skillfully applying the principles of trigonometry to create a seamless and captivating cylindrical environment.

But this journey is not just about code and algorithms; it's about unleashing your creativity. With each vertex, every texture coordinate, and all the carefully calculated normals, you've breathed life into your world, infusing it with your unique vision and style. You've turned lines of code into rolling hills, bustling towns, and endless horizons -- yup truly endless.

Remember that every creation is a stepping stone to further innovation. Armed with the knowledge gained from this tutorial series, you have the power to continue expanding your skills, exploring new techniques, and pushing the boundaries of what you can achieve in the realm of game development and 3D graphics.

(In the next tutorial series we will do something useful and create a player character with some trees)

StatusReleased
CategoryAssets
Rating
Rated 5.0 out of 5 stars
(2 total ratings)
Authorfrothzon
GenreAdventure
Tags2D, Animals, cylinder, world
Code licenseMIT License
Asset licenseCreative Commons Attribution v4.0 International

Download

Download NowName your own price

Click download now to get access to the following files:

cylinder_world_tutorial1.yyz 40 kB

Comments

Log in with itch.io to leave a comment.

Interesting timing -- I was just looking for something like this!

This is so cool! You’ve got the best gamemaker tutorials out there right now.