Skip to content

Commit

Permalink
move to go1.23rc1 minimum version.
Browse files Browse the repository at this point in the history
go1.23 has a fix for the windows Sleep bug discussed in issue #44343.
See golang/go#44343. This works so go1.23rc1
is the new minimum version.

Also move the icosphere demo code from the engine (vu/loader.go) to
the examples (eg/ps.go).
  • Loading branch information
gazed committed Jun 27, 2024
1 parent 85ffc8f commit 08cc550
Show file tree
Hide file tree
Showing 4 changed files with 191 additions and 166 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ Build and Test

**Build Dependencies**

* Go version 1.21 or later.
* Go version 1.23 or later.
* Vulkan version 1.3 or later, and vulkan validation layer from the SDK at https://www.lunarg.com/vulkan-sdk/
* OpenAL (https://openal.org) latest 64-bit version `soft_oal.dll` from https://openal-soft.org/openal-binaries/
* `glslc` executable from https:/google/shaderc is needed to build shaders in `vu/assets/shaders`.
Expand Down
190 changes: 186 additions & 4 deletions eg/ps.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,23 @@
package main

import (
"fmt"
"log/slog"
"math"
"time"

"github.com/gazed/vu"
"github.com/gazed/vu/load"
"github.com/gazed/vu/math/lin"
)

// ps primitive shapes explores creating geometric shapes and standard
// shape primitives using shaders. This example demonstrates:
// - loading assets.
// - creating a 3D scene.
// - controlling scene camera movement.
// - circle primitive shader and vertex line circle
// - generatede icospheres.
// - draw circles primitives, one from a shader, one from lines.
// - generate icosphere meshes using eng.MakeMesh.
//
// CONTROLS:
// - W,S : move forward, back
Expand Down Expand Up @@ -67,12 +71,12 @@ func ps() {
// create and draw an icosphere. At the lowest resolution this
// looks bad because the normals are shared where vertexes are
// part of multiple triangles.
eng.GenIcosphere(0)
genIcosphereMesh(eng, 0)
s0 := ps.scene.AddModel("shd:pbr0", "msh:icosphere0")
s0.SetAt(-3, 0, -10).SetColor(0, 0, 1, 1).SetMetallicRoughness(true, 0.2)

// a higher resolution icosphere starts to look ok with lighting.
eng.GenIcosphere(4)
genIcosphereMesh(eng, 4)
s2 := ps.scene.AddModel("shd:pbr0", "msh:icosphere4")
s2.SetAt(+3, 0, -10).SetColor(0, 1, 0, 1).SetMetallicRoughness(true, 0.2)

Expand Down Expand Up @@ -146,3 +150,181 @@ func (ps *pstag) limitPitch(pitch float64) float64 {
}
return pitch
}

// genIcosphereMesh creates a unit sphere made of triangles.
// Higher subdivisions create more triangles. Supported values are 0-7:
// - 0: 20 triangles
// - 1: 80 triangles
// - 2: 320 triangles
// - 3: 1280 triangles
// - 4: 5120 triangles
// - 5: 20_480 triangles
// - 6: 81_920 triangles
func genIcosphereMesh(eng *vu.Engine, subdivisions int) (err error) {
if subdivisions < 0 || subdivisions > 6 {
return fmt.Errorf("genIcosphereMesh: unsupported subdivision %d", subdivisions)
}

// create the initial icosphere mesh data.
verts, indexes := genIcosphere(subdivisions)

// generate triangle normals. This produces the same number of indexes
// and triangles but more vertexes since the vertexes are not shared
// between triangles - they each must have their own normal.
newVerts := []float32{}
normals := []float32{}
newIndexes := []uint16{}
for i := 0; i < len(indexes); i += 3 {
v1, v2, v3 := indexes[i], indexes[i+1], indexes[i+2]
p1x, p1y, p1z := verts[v1*3], verts[v1*3+1], verts[v1*3+2]
p2x, p2y, p2z := verts[v2*3], verts[v2*3+1], verts[v2*3+2]
p3x, p3y, p3z := verts[v3*3], verts[v3*3+1], verts[v3*3+2]
newVerts = append(newVerts, p1x, p1y, p1z)
newVerts = append(newVerts, p2x, p2y, p2z)
newVerts = append(newVerts, p3x, p3y, p3z)

// use midpoint of triangle as normal for the triangle vertexes.
mx := (p1x + p2x + p3x) / 3.0
my := (p1y + p2y + p3y) / 3.0
mz := (p1z + p2z + p3z) / 3.0
normal := lin.NewV3().SetS(float64(mx), float64(my), float64(mz)).Unit()
nx := float32(normal.X)
ny := float32(normal.Y)
nz := float32(normal.Z)
normals = append(normals, nx, ny, nz)
normals = append(normals, nx, ny, nz)
normals = append(normals, nx, ny, nz)

// same number of triangles... but now pointing to unique vertexes/normals.
newIndexes = append(newIndexes, uint16(i), uint16(i+1), uint16(i+2))
}

// load the generated data into a mesh using eng.MakeMesh.
meshData := make(load.MeshData, load.VertexTypes)
meshData[load.Vertexes] = load.F32Buffer(newVerts, 3)
meshData[load.Normals] = load.F32Buffer(normals, 3)
meshData[load.Indexes] = load.U16Buffer(newIndexes)
meshTag := fmt.Sprintf("icosphere%d", subdivisions)
return eng.MakeMesh(meshTag, meshData)
}

// genIcosphere creates mesh data for a unit sphere based on triangles.
// The number of vertexes increases with each subdivision.
// Based on:
// - http://blog.andreaskahler.com/2009/06/creating-icosphere-mesh-in-code.html
//
// The normals on a unit sphere nothing more than the direction from the
// center of the sphere to each vertex.
//
// Using uint16 for indexes limits the number of vertices to 65535.
//
// FUTURE: look at a slower icosphere subdivision that avoids exponential growth, see:
// https://devforum.roblox.com/t/hex-planets-dev-blog-i-generating-the-hex-sphere/769805
func genIcosphere(subdivisions int) (vertexes []float32, indexes []uint16) {
midPointCache := map[int64]uint16{} // stores new midpoint vertex indexes.

// addVertex is a closure that adds a vertex, ensuring that the
// vertex is on a unit sphere. Note the vertex is also the normal.
// Return the index of the vertex
addVertex := func(x, y, z float32) uint16 {
length := float32(math.Sqrt(float64(x*x + y*y + z*z)))
vertexes = append(vertexes, x/length, y/length, z/length)
return uint16(len(vertexes)/3) - 1 // indexes start at 0.
}

// getMidPoint is a closure that fetches or creates the
// midpoint index between indexes p1 and p2.
getMidPoint := func(p1, p2 uint16) (index uint16) {

// first check if the middle point has already been added as a vertex.
smallerIndex, greaterIndex := p1, p2
if p2 < p1 {
smallerIndex, greaterIndex = p2, p1
}
key := int64(smallerIndex)<<32 + int64(greaterIndex)
if val, ok := midPointCache[key]; ok {
return val
}

// not cached, then add a new vertex
p1X, p1Y, p1Z := vertexes[p1*3], vertexes[p1*3+1], vertexes[p1*3+2]
p2X, p2Y, p2Z := vertexes[p2*3], vertexes[p2*3+1], vertexes[p2*3+2]
midx := (p1X + p2X) / 2.0
midy := (p1Y + p2Y) / 2.0
midz := (p1Z + p2Z) / 2.0

// add vertex makes sure point is on unit sphere
index = addVertex(midx, midy, midz)

// cache the new midpoint and return index
midPointCache[key] = index
return index
}

// create initial 12 vertices of a icosahedron
// from the corners of 3 orthogonal planes.
t := float32((1.0 + math.Sqrt(5.0)) / 2.0)
addVertex(-1, +t, 0) // corners of XY-plane
addVertex(+1, +t, 0)
addVertex(-1, -t, 0)
addVertex(+1, -t, 0)
addVertex(0, -1, +t) // corners of YZ-plane
addVertex(0, +1, +t)
addVertex(0, -1, -t)
addVertex(0, +1, -t)
addVertex(+t, 0, -1) // corners of XZ-plane
addVertex(+t, 0, +1)
addVertex(-t, 0, -1)
addVertex(-t, 0, +1)

// create 20 triangles of the icosahedron
// 5 faces around point 0
indexes = append(indexes, 0, 11, 5)
indexes = append(indexes, 0, 5, 1)
indexes = append(indexes, 0, 1, 7)
indexes = append(indexes, 0, 7, 10)
indexes = append(indexes, 0, 10, 11)

// 5 adjacent faces
indexes = append(indexes, 1, 5, 9)
indexes = append(indexes, 5, 11, 4)
indexes = append(indexes, 11, 10, 2)
indexes = append(indexes, 10, 7, 6)
indexes = append(indexes, 7, 1, 8)

// 5 faces around point 3
indexes = append(indexes, 3, 9, 4)
indexes = append(indexes, 3, 4, 2)
indexes = append(indexes, 3, 2, 6)
indexes = append(indexes, 3, 6, 8)
indexes = append(indexes, 3, 8, 9)

// 5 adjacent faces
indexes = append(indexes, 4, 9, 5)
indexes = append(indexes, 2, 4, 11)
indexes = append(indexes, 6, 2, 10)
indexes = append(indexes, 8, 6, 7)
indexes = append(indexes, 9, 8, 1)

// create new triangles for each level of subdivision
for i := 0; i < subdivisions; i++ {

// create 4 new triangles to replace each existing triangle.
newIndexes := []uint16{}
for i := 0; i < len(indexes); i += 3 {
v1, v2, v3 := indexes[i], indexes[i+1], indexes[i+2]
a := getMidPoint(v1, v2) // create or fetch mid-point vertex.
b := getMidPoint(v2, v3) // ""
c := getMidPoint(v3, v1) // ""

newIndexes = append(newIndexes, v1, a, c)
newIndexes = append(newIndexes, v2, b, a)
newIndexes = append(newIndexes, v3, c, b)
newIndexes = append(newIndexes, a, b, c)
}

// replace the old indexes with the new ones.
indexes = newIndexes
}
return vertexes, indexes
}
148 changes: 0 additions & 148 deletions loader.go
Original file line number Diff line number Diff line change
Expand Up @@ -590,151 +590,3 @@ func generateDefaultTexture() (squareSize uint32, pixels []byte) {
}
return uint32(size), []byte(img.Pix)
}

// GenIcosphere creates a unit sphere made of triangles.
// Higher subdivisions create more triangles. Supported values are 0-7:
// - 0: 12 vertexes, 20 triangles
// - 1: 42 vertexes, 80 triangles
// - 2: 162 vertexes, 320 triangles
// - 3: 642 vertexes, 1280 triangles
// - 4: 2562 vertexes, 5120 triangles
// - 5: 10_242 vertexes, 20_480 triangles
// - 6: 40_962 vertexes, 81_920 triangles
// - 7: 163_842 vertexes, 327_680 triangles
func (eng *Engine) GenIcosphere(subdivisions int) (err error) {
if subdivisions < 0 || subdivisions > 7 {
return fmt.Errorf("GenIcoSphere: unsupported subdivision %d", subdivisions)
}

verts, indexes := genIcosphere(subdivisions)
var icosphereMeshData = make(load.MeshData, load.VertexTypes)
icosphereMeshData[load.Vertexes] = load.F32Buffer(verts, 3)
icosphereMeshData[load.Normals] = load.F32Buffer(verts, 3)
icosphereMeshData[load.Indexes] = load.U16Buffer(indexes)
m := newMesh(fmt.Sprintf("icosphere%d", subdivisions))
m.mid, err = eng.rc.LoadMesh(icosphereMeshData)
if err != nil {
return fmt.Errorf("LoadMesh %s: %w", m.label(), err)
}
eng.app.ld.assets[m.aid()] = m
slog.Debug("new asset", "asset", "msh:"+m.label(), "id", m.mid)
return nil
}

// genIcosphere creates mesh data for a unit sphere based on triangles.
// The number of vertexes increases with each subdivision.
// Based on:
// - http://blog.andreaskahler.com/2009/06/creating-icosphere-mesh-in-code.html
//
// The normals on a unit sphere nothing more than the direction from the
// center of the sphere to each vertex.
//
// Using uint16 for indexes limits the number of vertices to 65535.
func genIcosphere(subdivisions int) (vertexes []float32, indexes []uint16) {
midPointCache := map[int64]uint16{} // stores new midpoint vertex indexes.

// addVertex is a closure that adds a vertex, ensuring that the vertex is
// on a unit sphere. Note the vertex is also the normal.
// Returns the index of the vertex
addVertex := func(x, y, z float32) uint16 {
length := float32(math.Sqrt(float64(x*x + y*y + z*z)))
vertexes = append(vertexes, x/length, y/length, z/length)
return uint16(len(vertexes)/3) - 1 // indexes start at 0.
}

// getMidPoint is a closure that fetches or creates the
// midpoint index between indexes p1 and p2.
getMidPoint := func(p1, p2 uint16) (index uint16) {

// first check if the middle point has already been added as a vertex.
smallerIndex, greaterIndex := p1, p2
if p2 < p1 {
smallerIndex, greaterIndex = p2, p1
}
key := int64(smallerIndex)<<32 + int64(greaterIndex)
if val, ok := midPointCache[key]; ok {
return val
}

// not cached, then add a new vertex
p1X, p1Y, p1Z := vertexes[p1*3], vertexes[p1*3+1], vertexes[p1*3+2]
p2X, p2Y, p2Z := vertexes[p2*3], vertexes[p2*3+1], vertexes[p2*3+2]
midx := (p1X + p2X) / 2.0
midy := (p1Y + p2Y) / 2.0
midz := (p1Z + p2Z) / 2.0

// add vertex makes sure point is on unit sphere
index = addVertex(midx, midy, midz)

// cache the new midpoint and return index
midPointCache[key] = index
return index
}

// create initial 12 vertices of a icosahedron
// from the corners of 3 orthogonal planes.
t := float32((1.0 + math.Sqrt(5.0)) / 2.0)
addVertex(-1, +t, 0) // corners of XY-plane
addVertex(+1, +t, 0)
addVertex(-1, -t, 0)
addVertex(+1, -t, 0)
addVertex(0, -1, +t) // corners of YZ-plane
addVertex(0, +1, +t)
addVertex(0, -1, -t)
addVertex(0, +1, -t)
addVertex(+t, 0, -1) // corners of XZ-plane
addVertex(+t, 0, +1)
addVertex(-t, 0, -1)
addVertex(-t, 0, +1)

// create 20 triangles of the icosahedron
// 5 faces around point 0
indexes = append(indexes, 0, 11, 5)
indexes = append(indexes, 0, 5, 1)
indexes = append(indexes, 0, 1, 7)
indexes = append(indexes, 0, 7, 10)
indexes = append(indexes, 0, 10, 11)

// 5 adjacent faces
indexes = append(indexes, 1, 5, 9)
indexes = append(indexes, 5, 11, 4)
indexes = append(indexes, 11, 10, 2)
indexes = append(indexes, 10, 7, 6)
indexes = append(indexes, 7, 1, 8)

// 5 faces around point 3
indexes = append(indexes, 3, 9, 4)
indexes = append(indexes, 3, 4, 2)
indexes = append(indexes, 3, 2, 6)
indexes = append(indexes, 3, 6, 8)
indexes = append(indexes, 3, 8, 9)

// 5 adjacent faces
indexes = append(indexes, 4, 9, 5)
indexes = append(indexes, 2, 4, 11)
indexes = append(indexes, 6, 2, 10)
indexes = append(indexes, 8, 6, 7)
indexes = append(indexes, 9, 8, 1)

// create new triangles for each level of subdivision
for i := 0; i < subdivisions; i++ {

// create 4 new triangles to replace each existing triangle.
newIndexes := []uint16{}
for i := 0; i < len(indexes); i += 3 {
v1, v2, v3 := indexes[i], indexes[i+1], indexes[i+2]
a := getMidPoint(v1, v2) // create or fetch mid-point vertex.
b := getMidPoint(v2, v3) // ""
c := getMidPoint(v3, v1) // ""

newIndexes = append(newIndexes, v1, a, c)
newIndexes = append(newIndexes, v2, b, a)
newIndexes = append(newIndexes, v3, c, b)
newIndexes = append(newIndexes, a, b, c)
}

// replace the old indexes with the new ones.
indexes = newIndexes
}
return vertexes, indexes
}
Loading

0 comments on commit 08cc550

Please sign in to comment.