From 08cc5505eb941758e8e937506e98c0356228bcb4 Mon Sep 17 00:00:00 2001 From: rust Date: Thu, 27 Jun 2024 09:00:26 -0400 Subject: [PATCH] move to go1.23rc1 minimum version. go1.23 has a fix for the windows Sleep bug discussed in issue #44343. See https://github.com/golang/go/issues/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). --- README.md | 2 +- eg/ps.go | 190 ++++++++++++++++++++++++++++++++++++++++++++++++++++-- loader.go | 148 ------------------------------------------ vu.go | 17 ++--- 4 files changed, 191 insertions(+), 166 deletions(-) diff --git a/README.md b/README.md index 8779a12..010c223 100644 --- a/README.md +++ b/README.md @@ -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://github.com/google/shaderc is needed to build shaders in `vu/assets/shaders`. diff --git a/eg/ps.go b/eg/ps.go index d1abe1d..96ef18f 100644 --- a/eg/ps.go +++ b/eg/ps.go @@ -3,10 +3,14 @@ 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 @@ -14,8 +18,8 @@ import ( // - 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 @@ -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) @@ -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 +} diff --git a/loader.go b/loader.go index 86d2ad1..e6c71cb 100644 --- a/loader.go +++ b/loader.go @@ -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 -} diff --git a/vu.go b/vu.go index 7b3426e..722182a 100644 --- a/vu.go +++ b/vu.go @@ -217,24 +217,15 @@ func (eng *Engine) Run(updator Updator) { // frame complete, remember the start of this frame. previousFrameStart = frameStart - } - // sleep a bit to simulate other stuff happening. - busyWait(8 * time.Millisecond) + // Small sleep to rest the CPU. + // Requires go1.23+ to get 1ms pecision on windows. See go issue #44343. + time.Sleep(2 * time.Millisecond) // briefly release thread + } } eng.dispose() } -// busyWait is used to sleep for periods less than 15ms which is -// about the smallest granularity of the Sleep timer on windows. -// This is due to the time interval for the Windows interrupt timer. -func busyWait(t time.Duration) { - start := time.Now() - for time.Since(start) < t { - time.Sleep(0) // briefly release thread - } -} - // handleResize processes user window changes. func (eng *Engine) handleResize() { w, h := eng.dev.SurfaceSize() // display window size - updated