Posts
Search
Contact
Cookies
About
RSS

Python and raylib - a 3d template

Added 22 Sep 2020, 11 a.m. edited 18 Jun 2023, 1:12 a.m.

Last time we looked at a minimal example to provide a window displaying 2d text... Before more fully exploring raylib's 2D capabilities, we're going to have a quick look at a minimal 3d example!

Lets just look at the new bits....

camera = ffi.new('struct Camera3D *', [
    [1, 1, 3],
    [0, .5, 0],
    [0, 1, 0],
    45,
    rl.CAMERA_PERSPECTIVE
])

Using the ffi module we can create a camera structure, now you guessed it here's a fragment of raylib.h!

typedef struct Camera3D {
    Vector3 position;       // Camera position
    Vector3 target;         // Camera target it looks-at
    Vector3 up;             // Camera up vector (rotation over its axis)
    float fovy;             // Camera field-of-view apperture in Y (degrees) in perspective,  //used as near plane width in orthographic
    int type;               // Camera type, defines projection type: CAMERA_PERSPECTIVE or //CAMERA_ORTHOGRAPHIC
} Camera3D;

For example, you can see I set the type (CAMERA_PERSPECTIVE) and a field of view of 45 degrees. The next section of the initialisation creates and textures a model.

model = rl.LoadModelFromMesh(rl.GenMeshCube(1.0, 1.0, 1.0))
tx = rl.LoadTexture(b"data/texel_checker.png")
model.materials[0].maps[rl.MAP_DIFFUSE].texture = tx

While here we are "loading" a Model from a Mesh we've created, there is no reason you can't use rl.LoadModel(b"filename.xxx") where xxx is any of the 3D formats that are supported by raylib. I quite like binary GLTF files with embedded images (so it's all together), but here, as we are creating a Model from a Mesh we need to manually add a texture to it...

This image wants to be in the data directory

Note again the file name of the texure is sent as a binary string. When you create a model from a mesh, it will have texture information, but a texture that's a single white dot. Replace the materials MAP_DIFFUSE with our own texture and we get something more interesting.

    rl.UpdateCamera(camera)
    frame+=1

when updating the games various states, it's also the time to update the camera, to account for any change to the camera's position or orientation.

    model.transform = MatrixMultiply(model.transform,MatrixRotateX(0.002))[0]
    model.transform = MatrixMultiply(model.transform,MatrixRotateY(-0.004))[0]

Having imported raymath we get some matrix math utilities, which we're using to change the orientation of the model. The transform matrix has an effect on the position specified by the DrawModel function.

If we multiply a matrix by another one then it combines their effect, multiply the matrix by an X rotation and then again by a Y rotation, and the model twists and turns. It's important to remember that multiplying matrix A with matrix B doesn't have the same effect as B multiplied by A. Order of multiplication is important.

    rl.BeginDrawing()
    rl.ClearBackground(RAYWHITE)
    rl.BeginMode3D(camera[0])
    rl.DrawModel(model, [0,0.5,0], 1.0, WHITE)
    rl.DrawGrid(10, 1.0)
    rl.EndMode3D()
    
    rl.DrawFPS(10, 10)
    rl.DrawText(rl.TextFormat(b"Frame: %i",ffi.cast("int", frame)), 10, 28, 20, MAROON)

    rl.EndDrawing()

Like last time we begin drawing and clear the background, but this time there is an extra step, begin 3d mode takes a Camera, changing the 2D setup into a 3D setup.

Have a look at either the cheat sheet or raylib.h and see if you can work out what the parameters are doing with DrawModel, DrawGrid and DrawFPS.

After EndMode3D we drop back into 2D mode so its probably a good time to draw things like scores and so forth.

We will undoubtedly dive deeper into 3D at a later date, but in order to get to grips with the skill of coding in python and because raylib has such excellent 2D functionality, next time we'll be looking at 2D in more detail.

Heres the complete code, you'll also need raymath.py, oh and don't forget the texture above.

#!/usr/bin/env python3
from raylib.static import rl, ffi
from raylib.colors import *
from rlmath import *

#// Initialization
#//--------------------------------------------------------------------------------------
screenWidth = 800;
screenHeight = 450;

rl.SetConfigFlags(rl.FLAG_MSAA_4X_HINT| rl.FLAG_WINDOW_RESIZABLE)  # Enable Multi Sampling Anti Aliasing 4x (if available)
rl.SetTargetFPS(60)
rl.InitWindow(screenWidth, screenHeight, b"step 1")

camera = ffi.new('struct Camera3D *', [
    [1, 1, 3],
    [0, .5, 0],
    [0, 1, 0],
    45,
    rl.CAMERA_PERSPECTIVE
])

model = rl.LoadModelFromMesh(rl.GenMeshCube(1.0, 1.0, 1.0))
tx = rl.LoadTexture(b"data/texel_checker.png")
model.materials[0].maps[rl.MAP_DIFFUSE].texture = tx

frame = 0
# Main game loop
while not rl.WindowShouldClose():            #// Detect window close button or ESC key
    #// Update
    #//----------------------------------------------------------------------------------
    rl.UpdateCamera(camera)
    frame+=1
    
    model.transform = MatrixMultiply(model.transform,MatrixRotateX(0.002))[0]
    model.transform = MatrixMultiply(model.transform,MatrixRotateY(-0.004))[0]
    
    #//----------------------------------------------------------------------------------

    #// Draw
    #//----------------------------------------------------------------------------------
    rl.BeginDrawing()
    rl.ClearBackground(RAYWHITE)
    rl.BeginMode3D(camera[0])
    rl.DrawModel(model, [0,0.5,0], 1.0, WHITE)
    rl.DrawGrid(10, 1.0)
    rl.EndMode3D()
    
    rl.DrawFPS(10, 10)
    rl.DrawText(rl.TextFormat(b"Frame: %i",ffi.cast("int", frame)), 10, 28, 20, MAROON)

    rl.EndDrawing()

rl.UnloadTexture(tx)
rl.UnloadModel(model)