Skip to content

ZackWilde27/Z3dPy

Repository files navigation

Z3dPy


Z3dPy is my open-source 3D renderer written in Python, packaged into a module.

You'll need something to draw on, I recommend PyGame for it's speed.

  • It's Fast (fast enough that the speed will depend on your drawing method)
  • Easy to use
  • 100% Python, but can be sped up even further with an extension
  • Stores vertex animation efficiently with my Animated OBJ format.
  • Built-in features for making games, with the expandability to do more.

Z3dPyShowcase.mp4

Documentation can be found here.


Installation Guide

Download the zip folder, extract it somewhere, and create a new script in the same directory. Import it with:

import z3dpy as zp

Builds that end in an odd number are nightly builds, which may be unstable / lacking documentation (outside of the script itself)


Getting Started

example

(Don't mind the dithering, just a low quality GIF)

I'll use PyGame to drive the screen.

import z3dpy as zp
import pygame

pygame.init()
pygame.display.set_mode((1280, 720))

First, create a camera to view from, and give it the dimensions of the previously set up window.

myCamera = zp.Cam([0, 0, 0], 1280, 720)
# Vectors are lists or tuples, [x, y, z]

Now load a mesh to draw, I'll use the built-in suzanne monkey.

# Use the LoadMesh function to load an OBJ, DAE, or X3D file (filename, *vPos, *VScale)
myMesh = zp.LoadMesh("z3dpy/mesh/suzanne.obj", [0, 0, 2])
# Z is forward in this case, so it's placed in front of the camera

Parameters marked with a * are optional


Rendering can be broken down into 3 stages:

  • Set the internal camera
  • Rendering
  • Drawing (the method will depend on the module handling the screen)
# Set Internal Camera
zp.SetInternalCam(myCamera)

# Rastering
# For games I'd recommend combining meshes into Things, but for now just use RenderMeshList()
for tri in zp.RenderMeshList([myMesh]):
    pygame.draw.polygon(screen, zp.TriGetColour(tri), [(tri[0][0], tri[0][1]), (tri[1][0], tri[1][1]), (tri[2][0], tri[2][1])])

# Also update the display afterwards
pygame.display.flip()

If your display method doesn't have a native triangle drawing function, triangles can be converted into either lines or pixels (although the performance cost of looping through each line or pixel can be drastic, especially with the latter).

def MyPixelDraw(x, y)
    # Draw code here
    # The current triangle from the for loop can be accessed from here
    colour = zp.TriGetColour(tri)

for tri in zp.RenderMeshList([myMesh]):
    # Draw the triangles
    zp.TriToPixels(tri, MyPixelDraw)
    

or

def MyLineDraw(sx, sy, ex, ey):
   # Draw code here
   pygame.draw.line(screen, zp.TriGetColour(tri), (sx, sy), (ex, ey))

for tri in zp.RenderMeshList([myMesh]):
    # Draw the triangles
    zp.TriToLines(tri, MyLineDraw)
        

Right now, the mesh is using the default material, MATERIAL_UNLIT, which will just pass the colour of the triangle through.

image

To get something shaded, go back to where the mesh was defined and change it's material to either a built-in option or your own.

myMesh = zp.LoadMesh("z3dpy/mesh/suzanne.obj", [0, 0, 2])
# Z is forward in this case, so it's placed in front of the camera

# There are 4 built-in options:
# MATERIAL_UNLIT
# - colour is passed through
# MATERIAL_SIMPLE
# - shaded based on an isolated normal axis.
# MATERIAL_DYNAMIC
# - dynamic lighting, with the lights in z3dpy.lights
# MATERIAL_STATIC
# - shaded based on the tri's baked shade. Requires a BakeLighting() call.
myMesh.material = zp.MATERIAL_SIMPLE

With MATERIAL_SIMPLE it should look like this

image

More on materials can be found on the Meshes page


Lastly, chuck it in a loop.

# This only needs to be done per frame if the camera's going to move.
zp.SetInternalCam(myCamera)

# Raster Loop
while True:

    # Clear screen
    screen.fill("black")

    # Render 3D
    for tri in zp.RenderMeshList([myMesh]):
        pygame.draw.polygon(screen, zp.TriGetColour(tri), [(tri[0][0], tri[0][1]), (tri[1][0], tri[1][1]), (tri[2][0], tri[2][1])])

    # Update screen
    pygame.display.flip()
    
    # Rotate mesh
    myMesh.rotation = zp.VectorAdd(myMesh.rotation, [1, 2, 3])

Final Script:

import z3dpy as zp
import pygame


pygame.init()
screen = pygame.display.set_mode((1280, 720))

# Create a camera
# Cam(vPosition, *screenWidth, *screenHeight)
myCamera = zp.Cam([0, 0, 0], 1280, 720)

# Use the LoadMesh function to load an OBJ, DAE, or X3D file (filename, *vPos, *VScale)
myMesh = zp.LoadMesh("z3dpy/mesh/suzanne.obj", [0, 0, 2])

myMesh.material = zp.MATERIAL_SIMPLE

zp.SetInternalCam(myCamera)

# Render Loop

while True:
    # PyGame stuff to prevent freezing
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            pygame.quit()

    screen.fill("black")
    
    for tri in zp.RenderMeshList([myMesh]):
        pygame.draw.polygon(screen, zp.TriGetColour(tri), [(tri[0][0], tri[0][1]), (tri[1][0], tri[1][1]), (tri[2][0], tri[2][1])])

    pygame.display.flip()
    
    myMesh.rotation = zp.VectorAdd(myMesh.rotation, [1, 2, 3])

Exporting Mesh

Export your mesh as an OBJ, DAE, or X3D file.

  • UVs are supported, kinda.
  • LoadMesh() will automatically triangulate n-gons.
  • If materials are exported in an OBJ, LoadMesh() will automatically separate it into a list of meshes to be put in a Thing, and colour them based on the mtl file.

Up axis is -Y, and Forward axis is Z.

image