Skip to content

Commit

Permalink
Adding dynamic merging.
Browse files Browse the repository at this point in the history
Dynamic batching works by not flushing between batched object render calls. This means all objects will render "under" the merging object. They will take on its object / material level visual properties (object color, material color, texture filtering mode, texture, etc), but it's way more efficient than rendering them all individually.
  • Loading branch information
SolarLune committed Jul 7, 2022
1 parent 5d167b0 commit d5a3cc7
Show file tree
Hide file tree
Showing 9 changed files with 311 additions and 35 deletions.
107 changes: 97 additions & 10 deletions camera.go
Original file line number Diff line number Diff line change
Expand Up @@ -655,7 +655,34 @@ func (camera *Camera) Render(scene *Scene, models ...*Model) {

for _, model := range models {

if model.Mesh != nil {
if len(model.DynamicBatchModels) > 0 {

dynamicDepths := map[*Model]float64{}

transparent := false

for _, child := range model.DynamicBatchModels {

dynamicDepths[child] = camera.WorldToScreen(child.WorldPosition())[2]

for _, mp := range child.Mesh.MeshParts {
if child.isTransparent(mp) {
transparent = true
}
}
}

sort.SliceStable(model.DynamicBatchModels, func(i, j int) bool {
return dynamicDepths[model.DynamicBatchModels[i]] > dynamicDepths[model.DynamicBatchModels[j]]
})

if transparent {
transparents = append(transparents, renderPair{model, model.Mesh.MeshParts[0]})
} else {
solids = append(solids, renderPair{model, model.Mesh.MeshParts[0]})
}

} else if model.DynamicBatchOwner == nil && model.Mesh != nil {

for _, mp := range model.Mesh.MeshParts {
if model.isTransparent(mp) {
Expand Down Expand Up @@ -693,6 +720,8 @@ func (camera *Camera) Render(scene *Scene, models ...*Model) {

render := func(rp renderPair) {

startingVertexListIndex := vertexListIndex

model := rp.Model
meshPart := rp.MeshPart
mat := meshPart.Material
Expand Down Expand Up @@ -728,8 +757,6 @@ func (camera *Camera) Render(scene *Scene, models ...*Model) {

model.ProcessVertices(vpMatrix, camera, meshPart)

vertexListIndex := 0

backfaceCulling := true
if mat != nil {
backfaceCulling = mat.BackfaceCulling
Expand Down Expand Up @@ -820,7 +847,11 @@ func (camera *Camera) Render(scene *Scene, models ...*Model) {

// Enforce maximum vertex count; note that this is lazy, which is NOT really a good way of doing this, as you can't really know ahead of time how many triangles may render.
if vertexListIndex/3 >= ebiten.MaxIndicesNum/3 {
panic("error in rendering mesh [" + model.Mesh.Name + "] of model [" + model.name + "]. At " + fmt.Sprintf("%d", len(model.Mesh.Triangles)) + " triangles, it exceeds the maximum of 21845 rendered triangles total for one MeshPart; please break up the mesh into multiple MeshParts using materials, or split it up into models")
if model.DynamicBatchOwner == nil {
panic("error in rendering mesh [" + model.Mesh.Name + "] of model [" + model.name + "]. At " + fmt.Sprintf("%d", len(model.Mesh.Triangles)) + " triangles, it exceeds the maximum of 21845 rendered triangles total for one MeshPart; please break up the mesh into multiple MeshParts using materials, or split it up into models")
} else {
panic("error in rendering mesh [" + model.Mesh.Name + "] of model [" + model.name + "] underneath Dynamic merging owner " + model.DynamicBatchOwner.name + ". At " + fmt.Sprintf("%d", model.DynamicBatchOwner.DynamicBatchTriangleCount()) + " triangles, it exceeds the maximum of 21845 rendered triangles total for one MeshPart; please break up the mesh into multiple MeshParts using materials, or split it up into models")
}
}

colorVertexList[vertexListIndex].DstX = float32(p0[0])
Expand All @@ -843,11 +874,11 @@ func (camera *Camera) Render(scene *Scene, models ...*Model) {

}

if vertexListIndex == 0 {
if vertexListIndex == startingVertexListIndex {
return
}

vertexListIndex = 0
vertexListIndex = startingVertexListIndex

for _, tri := range meshPart.sortingTriangles {

Expand Down Expand Up @@ -965,6 +996,28 @@ func (camera *Camera) Render(scene *Scene, models ...*Model) {
indexList[i] = uint16(i)
}

}

flush := func(rp renderPair) {

if vertexListIndex == 0 {
return
}

model := rp.Model
meshPart := rp.MeshPart
mat := meshPart.Material

var img *ebiten.Image

if mat != nil {
img = mat.Texture
}

if img == nil {
img = defaultImg
}

// Render the depth map here
if camera.RenderDepth {

Expand Down Expand Up @@ -1068,10 +1121,30 @@ func (camera *Camera) Render(scene *Scene, models ...*Model) {

camera.DebugInfo.DrawnTris += vertexListIndex / 3

vertexListIndex = 0

}

for _, renderPair := range solids {
render(renderPair)
for _, pair := range solids {

// Internally, the idea behind dynamic batching is that we simply hold off on flushing until the
// end - this saves a lot of time if we're rendering singular low-poly objects, at the cost of each
// object sharing the same material / object-level properties (color / material blending mode, for
// example).
if dyn := pair.Model.DynamicBatchModels; len(dyn) > 0 {

for _, merged := range dyn {
for _, part := range merged.Mesh.MeshParts {
render(renderPair{Model: merged, MeshPart: part})
}
}

flush(pair)
} else {
render(pair)
flush(pair)
}

}

if len(transparents) > 0 {
Expand All @@ -1080,8 +1153,22 @@ func (camera *Camera) Render(scene *Scene, models ...*Model) {
return depths[transparents[i].Model] > depths[transparents[j].Model]
})

for _, renderPair := range transparents {
render(renderPair)
for _, pair := range transparents {

if dyn := pair.Model.DynamicBatchModels; len(dyn) > 0 {

for _, merged := range dyn {
for _, part := range merged.Mesh.MeshParts {
render(renderPair{Model: merged, MeshPart: part})
}
}

flush(pair)
} else {
render(pair)
flush(pair)
}

}

}
Expand Down
1 change: 0 additions & 1 deletion examples/lighting/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,6 @@ func (g *Game) Init() {
opt := tetra3d.DefaultGLTFLoadOptions()
opt.CameraWidth = g.Width
opt.CameraHeight = g.Height
opt.LoadBackfaceCulling = true
library, err := tetra3d.LoadGLTFData(gltfData, opt)
if err != nil {
panic(err)
Expand Down
2 changes: 1 addition & 1 deletion examples/logo/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@ func (g *Game) Draw(screen *ebiten.Image) {
if g.DrawDebugText {
g.Camera.DrawDebugText(screen, 1, colors.White())
txt := "F1 to toggle this text\nWASD: Move, Mouse: Look\nThe screen object shows what the\ncamera is looking at.\nF5: Toggle depth debug view\nF4: Toggle fullscreen\nESC: Quit"
text.Draw(screen, txt, basicfont.Face7x13, 0, 108, color.RGBA{255, 0, 0, 255})
text.Draw(screen, txt, basicfont.Face7x13, 0, 120, color.RGBA{200, 200, 200, 255})
}
}

Expand Down
4 changes: 4 additions & 0 deletions examples/stress/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,11 @@ import (
"github.com/kvartborg/vector"
"github.com/solarlune/tetra3d"
"github.com/solarlune/tetra3d/colors"
"golang.org/x/image/font/basicfont"

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

//go:embed testimage.png
Expand Down Expand Up @@ -265,6 +267,8 @@ func (g *Game) Draw(screen *ebiten.Image) {

if g.DrawDebugText {
g.Camera.DrawDebugText(screen, 1, colors.White())
txt := "F1 to toggle this text\nWASD: Move, Mouse: Look\nThis is a simple stress test.\nAll of the cubes are statically merged\ntogether into as few render calls as possible.\nF5: Toggle depth debug view\nF4: Toggle fullscreen\nESC: Quit"
text.Draw(screen, txt, basicfont.Face7x13, 0, 120, color.RGBA{200, 200, 200, 255})
}

}
Expand Down
Binary file added examples/stress2/character.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
108 changes: 102 additions & 6 deletions examples/stress2/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,19 @@ import (
"github.com/kvartborg/vector"
"github.com/solarlune/tetra3d"
"github.com/solarlune/tetra3d/colors"
"golang.org/x/image/font/basicfont"

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

//go:embed testimage.png
var testImage []byte

//go:embed character.png
var character []byte

type Game struct {
Width, Height int
Scene *tetra3d.Scene
Expand All @@ -38,13 +43,17 @@ type Game struct {
DrawDebugDepth bool
DrawDebugWireframe bool
DrawDebugNormals bool

Cubes []*tetra3d.Model

Time float64
}

func NewGame() *Game {

game := &Game{
Width: 398,
Height: 224,
Width: 796,
Height: 448,
PrevMousePosition: vector.Vector{},
}

Expand All @@ -55,29 +64,89 @@ func NewGame() *Game {

func (g *Game) Init() {

// OK, so in this stress test, we're rendering a lot of individual, low-poly objects (cubes).

// To make this run faster, we can batch objects together dynamically - unlike static batching,
// this will allow them to move and act individually while also allowing us to render them more
// efficiently as they will render in a single batch.
// However, dynamic batching has some limitations.

// 1) Dynamically batched Models can't have individual material / object properties (like object
// color, texture, material blend mode, or texture filtering mode). Vertex colors still
// work for fading individual triangles / a dynamically batched Model, though.

// 2) Dynamically batched objects all count up to a single vertex count, so we can't have
// more than the max number of vertices after batching all objects together into a single
// Model (which at this stage is 65535 vertices, or 21845 triangles).

// 3) Dynamically batched objects render using the batching object's first meshpart's material
// for rendering. This is to make it predictable as to how all batched objects appear at all times.

// 4) Because we're batching together dynamically batched objects, they can't write to the depth
// texture individually. This means dynamically batched objects cannot visually intersect with one
// another - they simply draw behind or in front of one another, and so are sorted according to their
// objects' depths in comparison to the camera.

// While there are some restrictions, the advantages are weighty enough to make it worth it - this
// functionality is very useful for things like drawing a lot of small, simple models, like NPCs,
// particles, or map icons.

// Create the scene (we're doing this ourselves in this case).
g.Scene = tetra3d.NewScene("Test Scene")

// Load the test cube texture.
img, _, err := image.Decode(bytes.NewReader(testImage))
if err != nil {
panic(err)
}

// We can reuse the mesh for all of the models.
// We can reuse the same mesh for all of the models.
cubeMesh := tetra3d.NewCube()

// Set up how the cube should appear.
mat := cubeMesh.MeshParts[0].Material
mat.Shadeless = true
mat.Texture = ebiten.NewImageFromImage(img)

// The batched model will hold all of our batching results. The batched objects keep their mesh-level
// details (vertex positions, UV, normals, etc), but will mimic the batching object's material-level
// and object-level properties (texture, material / object color, texture filtering, etc).

// When a model is dynamically batching other models, the batching model doesn't render.

// In this example, you will see it as a plane, floating above all the other Cubes before batching.

planeMesh := tetra3d.NewPlane()

// Load the character texture.
img, _, err = image.Decode(bytes.NewReader(character))
if err != nil {
panic(err)
}

mat = planeMesh.MeshParts[0].Material
mat.Shadeless = true
mat.Texture = ebiten.NewImageFromImage(img)

batched := tetra3d.NewModel(planeMesh, "DynamicBatching")
batched.Move(0, 4, 0)
batched.Rotate(1, 0, 0, tetra3d.ToRadians(90))
batched.Rotate(0, 1, 0, tetra3d.ToRadians(180))

g.Cubes = []*tetra3d.Model{}

for i := 0; i < 21; i++ {
for j := 0; j < 21; j++ {
// Create a new Model, position it, and add it to the cubes slice.
// Create a new Cube, position it, add it to the scene, and add it to the cubes slice.
cube := tetra3d.NewModel(cubeMesh, "Cube")
cube.SetLocalPosition(vector.Vector{float64(i * 3), 0, float64(-j * 3)})
cube.SetLocalPosition(vector.Vector{float64(i) * 1.5, 0, float64(-j * 3)})
g.Scene.Root.AddChildren(cube)
g.Cubes = append(g.Cubes, cube)
}
}

g.Scene.Root.AddChildren(batched)

g.Camera = tetra3d.NewCamera(g.Width, g.Height)
g.Camera.Far = 120
g.Camera.SetLocalPosition(vector.Vector{0, 0, 15})
Expand All @@ -93,6 +162,31 @@ func (g *Game) Update() error {

moveSpd := 0.1

tps := 1.0 / 60.0 // 60 ticks per second, regardless of display FPS

g.Time += tps

for cubeIndex, cube := range g.Cubes {
wp := cube.WorldPosition()

wp[1] = math.Sin(g.Time*math.Pi + float64(cubeIndex))

cube.SetWorldPosition(wp)
cube.Color.R = float32(cubeIndex) / 100
cube.Color.G = float32(cubeIndex) / 10
}

if inpututil.IsKeyJustPressed(ebiten.Key1) {
dyn := g.Scene.Root.Get("DynamicBatching").(*tetra3d.Model)

if len(dyn.DynamicBatchModels) == 0 {
dyn.DynamicBatchAdd(g.Cubes...) // Note that Model.DynamicBatchAdd() can return an error if batching the specified objects would push it over the vertex limit.
} else {
dyn.DynamicBatchRemove(g.Cubes...)
}

}

if ebiten.IsKeyPressed(ebiten.KeyEscape) {
err = errors.New("quit")
}
Expand Down Expand Up @@ -244,6 +338,8 @@ func (g *Game) Draw(screen *ebiten.Image) {

if g.DrawDebugText {
g.Camera.DrawDebugText(screen, 1, colors.White())
txt := "F1 to toggle this text\nWASD: Move, Mouse: Look\nStress Test 2 - Here, cubes are moving. We can render them\nefficiently by dynamically batching them, though they\nwill mimic the batching object (the character plane) visually - \nthey no longer have their own object color,\ntexture, blend mode, or texture filtering\n(as they all take these properties from the red cube).\nThey also can no longer intersect; rather, they\nwill just draw in front of or behind each other.\n1 Key: Toggle batching cubes together\nF5: Toggle depth debug view\nF4: Toggle fullscreen\nESC: Quit"
text.Draw(screen, txt, basicfont.Face7x13, 0, 140, color.RGBA{200, 200, 200, 255})
}

}
Expand All @@ -254,7 +350,7 @@ func (g *Game) Layout(w, h int) (int, int) {

func main() {

ebiten.SetWindowTitle("Tetra3d Test - Stress Test")
ebiten.SetWindowTitle("Tetra3d Test - Stress Test 2")
ebiten.SetWindowResizingMode(ebiten.WindowResizingModeEnabled)

game := NewGame()
Expand Down
Loading

0 comments on commit d5a3cc7

Please sign in to comment.