Tetra3D is a 3D software renderer written in Go and Ebiten and made for games.

Tetra3D

Tetra3D Logo Dark exploration

Tetra3D Docs

Support

If you want to support development, feel free to check out my itch.io / Steam / Patreon. I also have a Discord server here. Thanks~!

What is Tetra3D?

Tetra3D is a 3D software renderer written in Go by means of Ebiten, primarily for video games. Compared to a professional 3D rendering systems like OpenGL or Vulkan, it's slow and buggy, but it's also janky, and I love it for that.

It evokes a similar feeling to primitive 3D game consoles like the PS1, N64, or DS. Being that a software renderer is not nearly fast enough for big, modern 3D titles, the best you're going to get out of Tetra is drawing some 3D elements for your primarily 2D Ebiten game, or a relatively rudimentary fully 3D game (maybe something on the level of a PS1 or N64 game would be possible). That said, limitation breeds creativity, and I am intrigued at the thought of what people could make with Tetra.

Why did I make it?

Because there's not really too much of an ability to do 3D for gamedev in Go apart from g3n, go-gl and Raylib-go. I like Go, I like janky 3D, and so, here we are.

It's also interesting to have the ability to spontaneously do things in 3D sometimes. For example, if you were making a 2D game with Ebiten but wanted to display just a few things in 3D, Tetra3D should work well for you.

Finally, while a software renderer is not by any means fast, it is relatively simple and easy to use. Any platforms that Ebiten supports should also work for Tetra3D automatically (hopefully!).

Why Tetra3D? Why is it named that?

Because it's like a tetrahedron, a relatively primitive (but visually interesting) 3D shape made of 4 triangles. Otherwise, I had other names, but I didn't really like them very much. "Jank3D" was the second-best one, haha.

How do you get it?

go get github.com/solarlune/tetra3d

Tetra depends on kvartborg's vector package, takeyourhatoff's bitset package, and Ebiten itself for rendering. Tetra3D requires Go v1.16 or above.

How do you use it?

Make a camera, load a scene, render it. A simple 3D framework means a simple 3D API.

Here's an example:

package main

import (
	"errors"
	"fmt"
	"image/color"
	"math"
	"os"
	"runtime/pprof"
	"time"

	_ "image/png"

	"github.com/solarlune/tetra3d"

	"github.com/hajimehoshi/ebiten/v2"
)

const ScreenWidth = 398
const ScreenHeight = 224

type Game struct {
	Scene        *tetra3d.Scene
	Camera       *tetra3d.Camera
}

func NewGame() *Game {

	g := &Game{}

	// First, we load a scene from a .dae file. LoadDAEFile returns a *Scene 
	// and an error if it was unsuccessful. 
	scene, err := tetra3d.LoadDAEFile("examples.dae") 

	if err != nil {
		panic(err)
	}

	g.Scene = scene

	// If you need to rotate the model because the 3D modeler you're 
	// using doesn't use the same axes as Tetra (like Blender),
	// you can call Mesh.ApplyMatrix() to apply a rotation matrix 
	// (or any other kind of matrix) to the vertices, thereby rotating 
	// them and their triangles' normals. 

	// By default (if you're using Blender), this conversion is 
	// done for you; you can change this by passing a different
	// DaeLoadOptions parameter when calling tetra3d.LoadDAEFile().

	// Tetra uses OpenGL's coordinate system (+X = Right, +Y = Up, +Z = Back), 
	// in comparison to Blender's coordinate system (+X = Right, +Y = Forward, 
	// +Z = Up). 

	// Create a new Camera. We pass the size of the screen to the Camera so
	// it can create its own buffer textures (which are *ebiten.Images).
	g.Camera = tetra3d.NewCamera(ScreenWidth, ScreenHeight)

	// Place Models or Cameras using their Position properties (which is a 
	// 3D Vector from kvartborg's vector package). Cameras look forward 
	// down the -Z axis.
	g.Camera.Position[2] = 12

	return game
}

func (g *Game) Update() error { return nil }

func (g *Game) Draw(screen *ebiten.Image) {

	// Call Camera.Clear() to clear its internal backing texture. This
	// should be called once per frame before drawing your *Scene.
	g.Camera.Clear()

	// Now we'll render the Scene from the camera. The Camera's ColorTexture will then 
	// hold the result. 
	
	// We pass both the Scene and the Models because 1) the Scene influences
	// how Models draw (fog, for example), and 2) we may not want to render
	// all Models. We might want to filter out some Models, which you can do with
	// g.Scene.FilterModels().
	g.Camera.Render(g.Scene, g.Scene.Models) 

	// Before drawing the result, clear the screen first.
	screen.Fill(color.RGBA{20, 30, 40, 255})

	// Draw the resulting texture to the screen, and you're done! You can 
	// also visualize the depth texture with g.Camera.DepthTexture.
	screen.DrawImage(g.Camera.ColorTexture, nil) 

}

func (g *Game) Layout(w, h int) (int, int) {
	// This is the size of the window; note that a larger (or smaller) 
	// layout doesn't seem to impact performance very much.
	return ScreenWidth, ScreenHeight
}

func main() {

	game := NewGame()

	if err := ebiten.RunGame(game); err != nil {
		panic(err)
	}

}

That's basically it. Note that Tetra3D is, indeed, a work-in-progress and so will require time to get to a good state. But I feel like it works pretty well and feels pretty solid to work with as is. Feel free to examine the examples folder for a couple of examples showing how Tetra3D works - simply change to their directory and calling go run . should work.

What's missing?

The following is a rough to-do list (tasks with checks have been implemented):

  • 3D rendering
  • -- Perspective projection
  • -- Orthographic projection
  • -- Billboards
  • -- Some additional way to draw 2D stuff with no perspective changes (if desired) in 3D space
  • -- Basic depth sorting (sorting vertices in a model according to distance, sorting models according to distance)
  • -- A depth buffer and depth testing - This is now implemented by means of a depth texture and Kage shader, though the downside is that it requires rendering and compositing the scene into textures twice. Also, it doesn't work on triangles from the same object (as we can't render to the depth texture while reading it for existing depth).
  • -- A more advanced depth buffer - currently, the depth is written using vertex colors.
  • -- Offscreen Rendering
  • -- Model Batching / Combination (Ebiten should batch render calls together automatically, at least partially, as long as we adhere to the efficiency guidelines)
  • Culling
  • -- Backface culling
  • -- Frustum culling
  • -- Far triangle culling
  • -- Triangle clipping to view (this isn't implemented, but not having it doesn't seem to be too much of a problem for now)
  • Debug
  • -- Debug text: overall render time, FPS, render call count, vertex count, triangle count, skipped triangle count
  • -- Wireframe debug rendering
  • -- Normal debug rendering
  • Basic Single Image Texturing
  • -- Multitexturing?
  • -- Materials / Per-triangle images
  • -- Perspective-corrected texturing (currently it's affine, see Wikipedia)
  • Animations
  • -- Armature-based animations
  • -- Object transform-based animations
  • Scenes
  • -- Fog
  • -- Ambient vertex coloring
  • -- A node or scenegraph for parenting and simple visibility culling
  • DAE model loading
  • -- Vertex colors loading
  • -- UV map loading
  • -- Normal loading
  • -- Transform / full scene loading
  • Lighting?
  • Shaders
  • -- Normal rendering (useful for, say, screen-space shaders)
  • Basic Collisions
  • -- AABB collision testing / sphere collision testing?
  • -- Raycasting
  • Multithreading (particularly for vertex transformations)
  • Prefer Discrete GPU for computers with both discrete and integrated graphics cards

Again, it's incomplete and jank. However, it's also pretty cool!

Shout-out time~

Huge shout-out to the open-source community (i.e. StackOverflow, fauxgl, tinyrenderer, learnopengl.com, etc) at large for sharing the information and code to make this possible; I would definitely have never made this happen otherwise.

Owner
SolarLune
Independent game developer and programmer, owner of SolarLune Games
SolarLune
Comments
  • Lots of noise when displaying triangles

    Lots of noise when displaying triangles

    Hi I've tried running a few examples with the same results (see the attached).

    I'm running Go 1.17 on Debian Stable (5.10.0-10-amd64) via Xorg. The GPU on my Thinkpad is an HD 520.

    The video I've attached is of the animations example but the same thing happens on other examples. I've tried on the latest master branch and also checked out the v0.4.2 tag with the same results.

    I know there isn't much you can do remotely for such a weird issue but figured I would still notify you. I'm a go developer myself and have built toy renderers before, but I'm kinda at a loss for how to debug this. If there is something I can do to help, let me know.

    Install steps were just checkout, go mod tidy, go run ./examples/animation/main.go.

    https://user-images.githubusercontent.com/58468/149854580-e1c5f4a9-4fdc-4797-9eee-9db5260899b3.mp4

  • Buggy Z-order of billboard sprites when objects are close to each other

    Buggy Z-order of billboard sprites when objects are close to each other

    Repro is in this branch: https://github.com/eliasdaler/Tetra3d/tree/zorder_repro Check animatedTextures example

    When flying around, somewhere at this point you'll notice Z-fighting: image

    In my case, I can't place the objects apart from each other - Z-fighting like this happens when a player character gets near NPC characters and Z-fighting get pretty severe (and sometimes the Z-order is just wrong)

    image

  • Small objects get clipped even if I set camera.Near to very low values

    Small objects get clipped even if I set camera.Near to very low values

    Hello. I have noticed that some objects in my scene are disappearing once I get close to them. I thought that I could fix that by setting Near value lower, but unfortunately this didn't help. See the modified scene from shapes example.

    Left to Suzanne, there is an object which disappears if you get close to it. You can also see it being clipped even if you don't get close but rotate camera around sometimes. image

  • Capsule collision doesn't work with some triangle meshes

    Capsule collision doesn't work with some triangle meshes

    See repro here: https://github.com/eliasdaler/Tetra3d/tree/bounds_issues/examples/bounds

    When trying to climb a new surface I added with a capsule, collision doesn't work properly and the capsule doesn't ascend.

  • panic: Error: MeshPart.AddTriangles() not given enough vertices to construct complete triangles (i.e. multiples of 3 vertices).

    panic: Error: MeshPart.AddTriangles() not given enough vertices to construct complete triangles (i.e. multiples of 3 vertices).

    I'm noticing a trend in this package:

    func (part *MeshPart) AddTriangles(verts ...VertexInfo) {
    
    	mesh := part.Mesh
    
    	if part.TriangleEnd > -1 && part.TriangleEnd < mesh.triIndex {
    		panic("Error: Cannot add triangles to MeshPart non-contiguously (i.e. partA.AddTriangles(), partB.AddTriangles(), partA.AddTriangles() ).")
    	}
    
    	if len(verts) == 0 || len(verts)%3 > 0 {
    		panic("Error: MeshPart.AddTriangles() not given enough vertices to construct complete triangles (i.e. multiples of 3 vertices).")
    	}
    

    Is there any reason sanity checks constitute an entire program failure? why not restructure to..

    func (part *MeshPart) AddTriangles(verts ...VertexInfo) error {
    
    	mesh := part.Mesh
    
    	if part.TriangleEnd > -1 && part.TriangleEnd < mesh.triIndex {
    		return fmt.Errorf("cannot add triangles to MeshPart non-contiguously (i.e. partA.AddTriangles(), partB.AddTriangles(), partA.AddTriangles() ).")
    	}
    
    	if len(verts) == 0 || len(verts)%3 > 0 {
    		return fmt.Errorf("not enough vertices are provided to construct complete triangles (i.e. multiples of 3 vertices).")
    	}
    

    There are 26 uses of panic in this package: https://github.com/SolarLune/Tetra3d/search?q=panic And most aren't critical failures that require the entire application to exit immediately, most are simply malformed requests that can be reported back to abort current operation

  • Refactored examples by extracting common code on camera stuff

    Refactored examples by extracting common code on camera stuff

    I've been playing with the examples and I've seen there's some common code which could be extracted from them. I've created an ExampleGame struct with shared fields accross all examples (scene / cam info) and some methods to process the rotation / movement / setup of the camera.

    Some thoughts:

    • I'm not sure whether examples/base_example.go is the correct path to put this new code. I can change it to whatever path you seem fit.
    • The example shapes was not refactored as I didn't understand how to make a tetra3d.INode which is the type of the camera in that example to a tetra3d.Camera which is what most of the other examples use.
    • The example orthographic was also not changed as it treated the camera differently than the rest.
    • As I refactored several examples I began to think that ExampleGame might be a good candidate to use as a starter point for any tetra3d game one starts? It has a camera, and basic movement. Maybe rename it BaseGame?

    I think there's some more code (related to the debug options, quitting, full screen) in the examples which could be refactored. I might tackle that later.

  • Increase glTF parsing performance

    Increase glTF parsing performance

    First of all, great project!

    While testing your usage of qmuntal/gltf I noticed LoadGLTFData consumes quite a lot of memory compared to the size of the parsed glTF file. For example, ./example/logo/tetra3d.glb weights ~58KB and LoadGLTFData consumes ~3MB and produces ~27k allocations!

    My take is that most of the memory consumed by that function is accidental. A sane size to in-memory ratio would be 2 or 3x, not 58x.

    This PR contains several memory improvements, which as side effect also have some net speed gains:

    C:\Users\qmuntaldiaz\code\Tetra3d>benchstat old.txt new.txt
    name             old time/op    new time/op    delta
    LoadGLTFData-12    1.97ms ±10%    1.36ms ±15%  -30.67%  (p=0.000 n=9+9)
    
    name             old speed      new speed      delta
    LoadGLTFData-12  29.9MB/s ± 9%  43.1MB/s ±13%  +44.27%  (p=0.000 n=9+9)
    
    name             old B/op       new B/op       delta
    LoadGLTFData-12    3.17MB ± 0%    1.64MB ± 0%  -48.19%  (p=0.000 n=10+10)
    
    name             old allocs/op  new allocs/op  delta
    LoadGLTFData-12     27.6k ± 0%     25.5k ± 0%   -7.60%  (p=0.000 n=10+9)
    
  • Crashing when loading a mesh

    Crashing when loading a mesh

    Hello Tetra3D's gltf loader crashes with this message:

    panic: runtime error: index out of range [96] with length 96
    
    goroutine 1 [running, locked to thread]:
    github.com/solarlune/tetra3d.LoadGLTFData({0xa113e0, 0x76529, 0x76529}, 0xc000689f20)
    	<...>/Tetra3d/gltf.go:306 +0x4a45
    

    I've added a part of my model to shapes.blend so that you can reproduce it (hopefully) It can be found here: https://github.com/eliasdaler/Tetra3d/tree/gltf-crash/examples/shapes

    Here's the part that causes the problem:

    for triIndex, tri := range newMesh.Triangles {
    	tri.Normal = normals[triIndex] // <- here!
    }
    

    Adding a Print like this:

    fmt.Println(len(normals), len(indices), len(newMesh.Triangles), mesh.Name)
    

    shows this:

    96 288 608 Cylinder.003
    

    So, the number of triangles doesn't match number of indices... Maybe I'm doing something wrong with Blender, so if there's something I need to do with my models, please let me know! :)

  • update path to vector package

    update path to vector package

    Hi @SolarLune,

    Thanks for this cool project.

    Just creating this PR to let you know that I have changed my GitHub handle, this will impact the github.com/kvartborg/vector package, as it has changed to github.com/quartercastle/vector.

    The old package is still available and can be installed as usual, so it shouldn't effect projects dependent on your module. I can see that I have 33 depending on my vector package and most are indirect through your packages, I will strive post PR's for those to mitigate any problems that may arise when this has been merged.

    Sorry for the inconvenience.

  • recent releases aren't using recognized semantic version numbers

    recent releases aren't using recognized semantic version numbers

    ...and go mod is unhappy with them. Currently it believe v0.5.2 is the most recent release because its the most recent one with "acceptable" semantic versioning per https://go.dev/doc/modules/version-numbers

    To fix, re-release v0.8 as v0.8.0 or v0.8.1

  • Ensure colorShader always returns a color, regardless of depth

    Ensure colorShader always returns a color, regardless of depth

    @SolarLune I pretty sure this change doesn't actually fix the problem correctly, but wanted to at least show it to you. It does seem to fix the primary problem for me in all the examples I've tried. It seems like an illogical fix since the other fragment shaders aren't necessarily returning a fragment. Seems like its usually conditional.

    Even with this change there are visual depth issues. Like the z-buffer (or painters algorithm) is off. See screenshot. But I did confirm that these depth issues exist even without my changes. I'm not sure if its part of Tetra or if this is just an issue with my crappy video card. Either way the scene rendering looks good again, just something wrong with the depth buffer.

    I also notice in that same shader you are shadowing the color variable. Maybe that's intentional, but it seemed a little off to me. I don't know much past the basics of shaders and even less when it comes to Kage shaders.

    Check out the order of these boxes. image

    And clearly something with depth sorting here too

    https://user-images.githubusercontent.com/58468/150008395-0c8751f8-0722-4dce-898c-91d165894639.mp4

    This references #7

  • panic: runtime error: index out of range [2] with length 2

    panic: runtime error: index out of range [2] with length 2

    goroutine 1 [running, locked to thread]: github.com/solarlune/tetra3d.LoadGLTFData({0xc0000cc000, 0x6c2, 0x6c3}, 0x0) /Users/xackery/Documents/code/go/pkg/mod/github.com/solarlune/[email protected]/gltf.go:402 +0x82a7

    this time I can attach the gltf it fails with: interleaved.gltf.zip

    This is based on https://github.com/qmuntal/gltf/tree/80d3922a76d3e24371f8c99e0d213ecc8dae3ffe#data-interleaving

  • panic: index out of range

    panic: index out of range

    
    goroutine 1 [running, locked to thread]:
    github.com/solarlune/tetra3d.LoadGLTFData({0xc00044c000, 0x1d81b, 0x1d81b}, 0x0)
            /Users/xackery/Documents/code/go/pkg/mod/github.com/solarlune/[email protected]/gltf.go:162 +0x8e7a
    exit status 2
    

    seems:

    newMat.TexturePath = doc.Images[*doc.Textures[texture.Index].Source].URI
    

    is missing sanity checks and making assumptions, I'm using something similar to https://github.com/qmuntal/gltf/blob/80d3922a76d3e24371f8c99e0d213ecc8dae3ffe/modeler/example_test.go#L35 to generate a gltf and it is cutting corners on some steps, and causing tetra3d to panic non-gracefully

  • nil exception in gltf loader

    nil exception in gltf loader

    [signal SIGSEGV: segmentation violation code=0x1 addr=0x20 pc=0x4431201]
    
    goroutine 1 [running, locked to thread]:
    github.com/solarlune/tetra3d.LoadGLTFData({0xc000ad8000, 0x1d71f, 0x1d71f}, 0x0)
            /Users/xackery/Documents/code/go/pkg/mod/github.com/solarlune/[email protected]/gltf.go:158 +0xba1
    github.com/xackery/quail/cmd.(*viewer).load(0xc0004e4bf0, {0xc000ad8000?, 0xc0002b3c68?, 0x1?})
    

    seems

    if texture := gltfMat.PBRMetallicRoughness.BaseColorTexture; texture != nil {
    

    isn't happy with my hand crafted gltf files

Simple 2D-grid game made with Ebiten
Simple 2D-grid game made with Ebiten

Simple 2D-grid game made with Ebiten

Mar 15, 2022
Implementation of a popular graphics benchmark written on Ebiten.
Implementation of a popular graphics benchmark written on Ebiten.

Ebiten Bunny Mark This is an implementation of the popular graphics benchmark written on Ebiten. The initial benchmark was created by Ian Lobb (code)

Dec 7, 2022
Helper library to transform TMX tile maps into a simpler format for Ebiten

Ebitmx Ebitmx is a super simple parser to help render TMX maps when using Ebiten for your games. Right now is super limited to XML and CSV data struct

Nov 16, 2022
Arkanoid game in Go using Ebiten game engine with ECS.
Arkanoid game in Go using Ebiten game engine with ECS.

Arkanoid-go Arkanoid game in Go using Ebiten game engine with ECS. You must have Git LFS installed when cloning the repository to download assets. See

Oct 9, 2022
A simple game that I created with Ebiten game library as a way to teach myself Go. Enjoy!
A simple game that I created with Ebiten game library as a way to teach myself Go. Enjoy!

galactic-asteroid-belt Overview A simple game that I created with Ebiten game library as a way to teach myself Go. Enjoy! Run To run, you will need Go

Dec 2, 2021
Minimal polymorphic solitaire engine in Go, ebiten

Gilbert Oddstream's Minimal Polymorphic Solitaire 5 There's a live WASM version here. Towards a polymorphic solitaire engine in Go+Ebiten, with help f

Dec 14, 2022
Donburi is just another Entity Component System library for Ebiten inspired by legion.
Donburi is just another Entity Component System library for Ebiten inspired by legion.

Donburi Donburi is just another Entity Component System library for Ebiten inspired by legion. It aims to be a feature rich and high performance ECS L

Dec 15, 2022
Spaceshooter - A port to go of the pygame Space Shooter game using the ebiten library
Spaceshooter - A port to go of the pygame Space Shooter game using the ebiten library

Space Shooter This is a port to go of the pygame Space Shooter (https://github.c

Sep 29, 2022
Dedicated Game Server Hosting and Scaling for Multiplayer Games on Kubernetes
Dedicated Game Server Hosting and Scaling for Multiplayer Games on Kubernetes

Agones is a library for hosting, running and scaling dedicated game servers on Kubernetes. Agones, is derived from the Greek word agōn which roughly t

Jan 6, 2023
Awesome 2D Maze-based games to play with kids in family and with friends on rich console UI. developed into Go.
Awesome 2D Maze-based games to play with kids in family and with friends on rich console UI. developed into Go.

gomazes Have fun time with kids and family and friends at playing awesome 2D maze-based games while feeling like a programmer on the computer console/

Dec 23, 2021
Open source of the build infrastructure used by Stadia Games & Entertainment

SG&E Monorepo This repository contains the open sourcing of the infrastructure developed by Stadia Games & Entertainment (SG&E) to run its operations.

Dec 18, 2022
This project is designed to be an open source implementation for streaming desktop games using WebRTC
This project is designed to be an open source implementation for streaming desktop games using WebRTC

The aim of this project is develop a WebRTC screenshare designed for streaming video games and accepting remote inputs. There will be ansible instruct

Oct 6, 2022
Tetris in Go, as a test in using Go for small games

Tetris in Go This is an attempt at implementing Tetris in Go in a way which has

May 8, 2022
Using finite projective planes to make card (maybe video) games

pairwise What it is Using finite projective plane to generate card (maybe video) games. Running Run with go run . Right now uses Go 1.17 but 1.18 just

Jan 24, 2022
Tcell-game-template - A small template repository for simple tcell based games

tcell game template This is a template repository used for making small terminal

Jan 22, 2022
Minecraft Bedrock Edition server software written in Go
Minecraft Bedrock Edition server software written in Go

Dragonfly Dragonfly is a heavily asynchronous server software for Minecraft Bedrock Edition written in Go.

Jan 3, 2023
Minecraft server made in go, faster and better!

ElytraGo Minecraft server made in go, faster and better! Project is in early stage, but I'm trying continuously update it with new lines of code :)) L

Dec 9, 2022
Snake game made in Go! 🐍
Snake game made in Go! 🐍

Snake This is a Terminal based snake game made by tristangoossens. Please star this repository to help my first big project grow! Documentation can be

Nov 16, 2022
A simple chess engine for experiment, made in Golang

chess-engine A simple chess engine for experiment, made in Golang Build the engine Run the command make or make build Run the engine Run the command f

Dec 26, 2021