Skip to content

Commit

Permalink
Small additions and changes for current project.
Browse files Browse the repository at this point in the history
o Add icosphere to the engine basic object types.
o Add quaternion RotateTo method.
  Calculates rotation needed to go from one direction vector to another.
o Add MakeMesh to allow apps to add generated meshes.
o Flip distance order for transparent objects.
  • Loading branch information
gazed committed Jun 26, 2024
1 parent 2c47ccd commit 85ffc8f
Show file tree
Hide file tree
Showing 6 changed files with 202 additions and 6 deletions.
18 changes: 17 additions & 1 deletion eg/ps.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
// - creating a 3D scene.
// - controlling scene camera movement.
// - circle primitive shader and vertex line circle
// - generatede icospheres.
//
// CONTROLS:
// - W,S : move forward, back
Expand All @@ -40,12 +41,15 @@ func ps() {
// import assets from asset files.
// This creates the assets referenced by the models below.
// Note that circle and quad meshes engine defaults.
eng.ImportAssets("circle.shd", "lines.shd")
eng.ImportAssets("circle.shd", "lines.shd", "pbr0.shd")

// The scene holds the cameras and lighting information
// and acts as the root for all models added to the scene.
ps.scene = eng.AddScene(vu.Scene3D)

// add one directional light. SetAt sets the direction.
ps.scene.AddLight(vu.DirectionalLight).SetAt(-1, -2, -2)

// Draw a 3D line circle using a shader and a quad.
scale := 3.0
c1 := ps.scene.AddModel("shd:circle", "msh:quad")
Expand All @@ -60,6 +64,18 @@ func ps() {
c3.SetAt(+3.0, 0, -5).SetScale(scale/2, scale/2, scale/2)
c3.SetColor(1, 0, 0, 1) // red

// 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)
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)
s2 := ps.scene.AddModel("shd:pbr0", "msh:icosphere4")
s2.SetAt(+3, 0, -10).SetColor(0, 1, 0, 1).SetMetallicRoughness(true, 0.2)

eng.Run(ps) // does not return while example is running.
}

Expand Down
148 changes: 148 additions & 0 deletions loader.go
Original file line number Diff line number Diff line change
Expand Up @@ -590,3 +590,151 @@ 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
}
17 changes: 17 additions & 0 deletions math/lin/quaternion.go
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,23 @@ func (q *Q) SetAa(ax, ay, az, angle float64) *Q {
return q
}

// RotateTo returns a rotation to rotate from direction vector v1
// to direction vector v2.
//
// from https://stackoverflow.com/questions/1171849/finding-quaternion-representing-the-rotation-from-one-vector-to-another
func (q *Q) RotateTo(v1, v2 *V3) *Q {
k_cos_theta := v1.Dot(v2)
k := math.Sqrt(v1.LenSqr() * v2.LenSqr())
if k_cos_theta/k == -1 {
// 180 degree rotation around orthogonal vector.
o := NewV3().Cross(v1, v2) // orthogonal vector
return q.SetS(0, o.X, o.Y, o.Z).Unit()
}
axis := NewV3().Cross(v1, v2)
angle := k_cos_theta + k
return q.SetS(axis.X, axis.Y, axis.Z, angle).Unit()
}

// Roll, Pitch, Yaw from one of the answers to
// https://stackoverflow.com/questions/5782658/extracting-yaw-from-a-quaternion
// switch the answer Z:Yaw, Y:Pitch, X:Roll to be X:Pitch, Y:Yaw, Z:Roll
Expand Down
11 changes: 6 additions & 5 deletions model.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ package vu
import (
"fmt"
"log/slog"
"math"
"strings"

"github.com/gazed/vu/load"
Expand Down Expand Up @@ -343,7 +344,7 @@ func (m *model) fillPacket(packet *render.Packet, pov *pov, cam *Camera) bool {
packet.Uniforms[load.SCALE] = render.V4SToBytes(sx, sy, sz, 0, packet.Uniforms[load.SCALE])
case load.COLOR, load.MATERIAL:
if m.mat == nil {
slog.Debug("model waiting on materials")
slog.Debug("model waiting on materials", "shader", m.shader.name)
return false // material not yet loaded or set.
}
// Set the model material uniform data.
Expand All @@ -352,11 +353,11 @@ func (m *model) fillPacket(packet *render.Packet, pov *pov, cam *Camera) bool {
metal, rough := m.mat.metallic, m.mat.roughness
packet.Uniforms[load.MATERIAL] = render.V4S32ToBytes(metal, rough, 0, 0, packet.Uniforms[load.MATERIAL])
default:
// basic uniforms are set using SetUniform on the model.
// basic uniforms are set using SetModelUniform.
data, ok := m.uniforms[u.PacketUID]
if !ok {
slog.Debug("model waiting on uniform data")
return false // uniform data has not yet be set with SetUniform.
slog.Debug("model waiting on uniform data", "shader", m.shader.name)
return false // uniform data has not yet be set with SetModelUniform.
}
packet.Uniforms[u.PacketUID] = packet.Uniforms[u.PacketUID][:0]
packet.Uniforms[u.PacketUID] = append(packet.Uniforms[u.PacketUID], data...)
Expand All @@ -373,7 +374,7 @@ func (m *model) fillPacket(packet *render.Packet, pov *pov, cam *Camera) bool {
packet.Bucket = setBucketType(packet.Bucket, drawTransparent)
}
packet.Bucket = setBucketShader(packet.Bucket, m.shader.sid)
packet.Bucket = setBucketDistance(packet.Bucket, m.tocam)
packet.Bucket = setBucketDistance(packet.Bucket, math.MaxFloat64-m.tocam)
return true // model has all information needed to render.
}

Expand Down
1 change: 1 addition & 0 deletions scene.go
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,7 @@ func newBucket(pass render.PassID) uint64 {
}

// setBucketDistance to camera for sorting transparent objects.
// closer objects should be drawn last.
func setBucketDistance(b uint64, toCam float64) uint64 {
return b | uint64(math.Float32bits(float32(toCam)))
}
Expand Down
13 changes: 13 additions & 0 deletions vu.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import (

"github.com/gazed/vu/audio"
"github.com/gazed/vu/device"
"github.com/gazed/vu/load"
"github.com/gazed/vu/render"
)

Expand Down Expand Up @@ -274,6 +275,18 @@ func (eng *Engine) SetResizeListener(resizer Resizer) {
eng.app.resizer = resizer
}

// MakeMesh loads application generated mesh data.
func (eng *Engine) MakeMesh(uniqueName string, meshData load.MeshData) (err error) {
m := newMesh(uniqueName)
m.mid, err = eng.rc.LoadMesh(meshData)
if err != nil {
return fmt.Errorf("MakeMesh %s: %w", uniqueName, err)
}
eng.app.ld.assets[m.aid()] = m
slog.Debug("new asset", "asset", "msh:"+m.label(), "id", m.mid)
return nil
}

// Shutdown is an application request to close down the engine.
// Mark the engine as shutdown which will cause the game loop to exit.
func (eng *Engine) Shutdown() {
Expand Down

0 comments on commit 85ffc8f

Please sign in to comment.