Canvas3D Game Source section: `https://appsudo.com/doc#canvas3d-game` Canvas3D records drawing commands in Python. The SDK draws those commands. This guide focuses on the CubeGfx example and its lifecycle. Use the [3D Engine Canvas3D Foundation](https://appsudo.com/doc#engine3d-foundation) for the complete lower-level reference. Canvas3D Game Source section: `https://appsudo.com/doc#canvas3d-game` Canvas3D records drawing commands in Python. The SDK draws those commands. A game needs a `Canvas3D`, a `GameWindow3D` subclass, and the `Device` passed to its callbacks. ## Rules to follow 1. Create every resource in `on_create` and destroy it in `on_destroy`. `on_create` can run again, and earlier resources are then invalid. 2. Record drawing only inside one `with gfx.frame(...)` block per `on_draw` tick. 3. Pipelines compile asynchronously. Check `pipeline.ready`, use the boolean returned by draw calls, or call `gfx.warmup(...)`. Inspect `pipeline.failed` and `pipeline.error` when compilation fails. 4. File textures load asynchronously. Check `texture.ready` or use `texture.on_ready(...)` when readiness matters. 5. Buffer data must be `array.array`, `bytes`, or `bytearray`. Plain lists raise `GfxError`. 6. Use 16-bit indices for at most 65536 vertices and 32-bit indices for larger meshes. 7. A vertex layout must provide every attribute read by the shader at the fixed `STANDARD_LOCATIONS`. Its stride must match the data. 8. Uniform names, order, and types must match the shader block exactly. A `vec3` consumes 16 bytes of alignment in std140. 9. Pass `zero_to_one=gfx.clip_zero_to_one` to projection builders, rebuild projection matrices in `on_resize`, and combine matrices as `projection @ view @ model`. 10. Set MSAA, depth bits, stencil, transparency, debug, and the app directory before the canvas is first shown. 11. Touch coordinates and surface sizes are pixels. Read the current width and height. 12. Custom shaders must include the GLSL pair and the msl source, with equivalent results. 13. Textures use straight alpha. Use `Blend.alpha()` for normal transparency. Draw opaque geometry first, then transparent geometry with `depth_write=False`. 14. Texture dimensions cannot exceed `gfx.max_texture_size`. 15. Do not block in callbacks. Scale motion by `dt`. 16. Release resources with `destroy()`. Using a destroyed resource raises `GfxError`. 17. Frame clear colors use values from 0 to 1. 18. Draw-call order determines drawing order. There is no sorting or scene graph. ## Imports Canvas3D games use these SDK namespaces: ```python from appsudo import App from components.canvas3d import Canvas3D from components.extras.game3d.window import GameWindow3D, Touch, TouchEvent from components.extras.gfx import BufferUsage, VertexLayout from components.extras.math3d import Mat4, Vec3 ``` ## Quick start: CubeGfx The example expects `res/textures/crate.png` inside the app directory. ![CubeGfx preview showing a rotating textured crate](/doc/assets/canvas3d-cubegfx-preview.png) ```python from array import array from appsudo import App from components.canvas3d import Canvas3D from components.extras.game3d.window import GameWindow3D, Touch from components.extras.gfx import VertexLayout, BufferUsage from components.extras.math3d import Mat4, Vec3 def make_cube(): verts, idx = [], [] faces = [((0, 0, 1), (1, 0, 0), (0, 1, 0)), ((0, 0, -1), (-1, 0, 0), (0, 1, 0)), ((1, 0, 0), (0, 0, -1), (0, 1, 0)), ((-1, 0, 0), (0, 0, 1), (0, 1, 0)), ((0, 1, 0), (1, 0, 0), (0, 0, -1)), ((0, -1, 0), (1, 0, 0), (0, 0, 1))] for n, u, v in faces: base = len(verts) // 8 for su, sv, tu, tv in ((-1, -1, 0, 1), (1, -1, 1, 1), (1, 1, 1, 0), (-1, 1, 0, 0)): pos = [(n[i] + su * u[i] + sv * v[i]) * 0.5 for i in range(3)] verts += pos + list(n) + [tu, tv] idx += [base, base + 1, base + 2, base, base + 2, base + 3] return array("f", verts), array("H", idx) class CubeGfx(GameWindow3D): def __init__(self, canvas, application_path): super().__init__(canvas) self.path = application_path self.yaw, self.pitch, self.last_touch = 30.0, 20.0, None self.projection = self.view = None def on_create(self, gfx): verts, indices = make_cube() self.index_count = len(indices) self.pipeline = gfx.pipeline( shader="builtin/lit_textured", vertex=VertexLayout(position="f32x3", normal="f32x3", uv="f32x2"), depth=True, cull="back", blend=None, color_format=gfx.surface_format) self.vbo = gfx.buffer(verts, BufferUsage.VERTEX) self.ibo = gfx.buffer(indices, BufferUsage.INDEX) self.uniforms = gfx.uniforms(mvp="mat4", model="mat4", light_dir="vec3") self.texture = gfx.texture_from_file("res/textures/crate.png", mipmaps=True) self.bindings = gfx.bind(self.pipeline, uniforms=self.uniforms, tex=self.texture) self.view = Mat4.look_at(Vec3(0, 1.5, 4), Vec3(0, 0, 0), Vec3(0, 1, 0)) self.projection = Mat4.perspective(60.0, gfx.aspect, 0.1, 100.0, zero_to_one=gfx.clip_zero_to_one) gfx.warmup([self.pipeline]) def on_resize(self, gfx, width, height): self.projection = Mat4.perspective(60.0, width / height, 0.1, 100.0, zero_to_one=gfx.clip_zero_to_one) def on_draw(self, gfx, dt): if not self.pipeline.ready: return if self.last_touch is None: self.yaw += 40.0 * dt model = Mat4.rotation_y(self.yaw) @ Mat4.rotation_x(self.pitch) self.uniforms.set(mvp=self.projection @ self.view @ model, model=model, light_dir=(-0.4, -0.8, -0.5)) with gfx.frame(clear=(0.05, 0.07, 0.10, 1.0)) as frame: frame.draw_indexed(self.pipeline, self.bindings, self.vbo, self.ibo, self.index_count) def on_touch(self, action, x, y): if action == Touch.DOWN: self.last_touch = (x, y) elif action == Touch.MOVE and self.last_touch: self.yaw += (x - self.last_touch[0]) * 0.5 self.pitch += (y - self.last_touch[1]) * 0.5 self.last_touch = (x, y) elif action in (Touch.UP, Touch.CANCEL): self.last_touch = None def on_destroy(self, gfx): for name in ("vbo", "ibo", "texture", "uniforms", "pipeline"): res = getattr(self, name, None) if res is not None: res.destroy() class CubeGfxApp(App): def __init__(self, application_path): App.__init__(self, "CubeGfx", application_path) self.setup() def setup(self): self.ui_view = Canvas3D() self.ui_view.set_app_dir(self.application_path) self.game = CubeGfx(self.ui_view, self.application_path) def run(self): App.run(self) self.render(self.ui_view.render()) def get_gamepad(self): return None def on_close(self): self.game.destroy() ``` `make_cube()` writes eight floats per vertex: three for position, three for normal, and two for UV. That matches the `VertexLayout` exactly. `array("H")` stores 16-bit indices, which is enough for this 24-vertex mesh. `on_create` creates all resources and can safely replace them after recreation. `on_resize` rebuilds the projection for the current aspect ratio. `on_draw` returns until the pipeline is ready, advances rotation using `dt`, updates uniforms, and records one frame. Touch drags update yaw and pitch in pixels. `on_destroy` releases each owned resource. ## Important things to note 1. Create every resource in `on_create` and destroy it in `on_destroy`. `on_create` can run again, and earlier resources are then invalid. 2. Record drawing only inside one `with gfx.frame(...)` block per `on_draw` tick. 3. Pipelines compile asynchronously. Check `pipeline.ready`, use the boolean returned by draw calls, or call `gfx.warmup(...)`. Inspect `pipeline.failed` and `pipeline.error` when compilation fails. 4. File textures load asynchronously. Check `texture.ready` or use `texture.on_ready(...)` when readiness matters. 5. Buffer data must be `array.array`, `bytes`, or `bytearray`. Plain lists raise `GfxError`. 6. Use 16-bit indices for at most 65536 vertices and 32-bit indices for larger meshes. 7. A vertex layout must provide every attribute read by the shader at the fixed `STANDARD_LOCATIONS`. Its stride must match the data. 8. Uniform names, order, and types must match the shader block exactly. A `vec3` consumes 16 bytes of alignment in std140. 9. Pass `zero_to_one=gfx.clip_zero_to_one` to projection builders, rebuild projection matrices in `on_resize`, and combine matrices as `projection @ view @ model`. 10. Set MSAA, depth bits, stencil, transparency, debug, and the app directory before the canvas is first shown. 11. Touch coordinates and surface sizes are pixels. Read the current width and height. 12. Custom shaders must include the GLSL pair and the msl source, with equivalent results. 13. Textures use straight alpha. Use `Blend.alpha()` for normal transparency. Draw opaque geometry first, then transparent geometry with `depth_write=False`. 14. Texture dimensions cannot exceed `gfx.max_texture_size`. 15. Do not block in callbacks. Scale motion by `dt`. 16. Release resources with `destroy()`. Using a destroyed resource raises `GfxError`. 17. Frame clear colors use values from 0 to 1. 18. Draw-call order determines drawing order. There is no sorting or scene graph. ## Common mistakes and fixes | Mistake | Fix | | --- | --- | | Passing a Python list to `gfx.buffer` | Convert vertices to `array("f", values)` and indices to `array("H", values)` or `array("I", values)`. | | Omitting `zero_to_one` | Pass `zero_to_one=gfx.clip_zero_to_one` and rebuild the projection in `on_resize`. | | Drawing before a pipeline is ready | Check `pipeline.ready`, handle a false draw return, or warm up the pipeline. Inspect failure fields when needed. | | Mismatching uniform fields | Match the shader block names, order, and std140 types exactly in `gfx.uniforms`. | | Creating resources outside `on_create` | Create all resources in `on_create` because it can run again after recreation. | | Keeping resources across `on_create` calls | Replace them and release them in `on_destroy`. Earlier resources are invalid. | | Loading a relative texture without an app directory | Call `canvas.set_app_dir(...)` before the canvas is first shown. | | Omitting the msl source | Ship the GLSL pair and the equivalent msl source in every custom `ShaderSource`. | | Using a destroyed resource | Stop using it immediately after `destroy()`. Recreate it in the next `on_create`. | | Using the wrong vertex stride | Make the `VertexLayout` formats and order match the packed data exactly. | | Omitting a shader attribute | Provide every shader attribute in the layout at its `STANDARD_LOCATIONS` location. | | Blocking in `on_draw` | Move heavy work elsewhere and scale game motion by `dt`. | | Treating file textures as immediately ready | Check `texture.ready` or register `on_ready`. | | Drawing transparent geometry first | Draw opaque geometry first, then use `Blend.alpha()` with `depth_write=False` for transparent geometry. | ## API reference See [Canvas3D Foundation](https://appsudo.com/doc#engine3d-foundation) for Canvas3D, GameWindow3D, Device, Frame, resources, shaders, meshes, math, events, and errors. ## Related guides - [Canvas3D + 2D Engine](/doc/llm-doc/canvas3d-2d-engine/llms.txt): Worked rpg2d guide for sprites, tilemaps, 2D physics, lights, and shadows.