eq2go/internal/zone/raycast/raycast.go

590 lines
16 KiB
Go

package raycast
import (
"fmt"
"math"
"sync"
)
// RaycastMesh provides high-speed raycasting against triangle meshes using AABB trees
// This is a Go implementation based on the C++ raycast mesh system
type RaycastMesh struct {
vertices []float32 // Vertex positions (x,y,z,x,y,z,...)
indices []uint32 // Triangle indices (i1,i2,i3,i4,i5,i6,...)
grids []uint32 // Grid IDs for each triangle
widgets []uint32 // Widget IDs for each triangle
triangles []*Triangle // Processed triangles
root *AABBNode // Root of the AABB tree
boundMin [3]float32 // Minimum bounding box
boundMax [3]float32 // Maximum bounding box
maxDepth uint32 // Maximum tree depth
minLeafSize uint32 // Minimum triangles per leaf
minAxisSize float32 // Minimum axis size for subdivision
mutex sync.RWMutex
}
// Triangle represents a single triangle in the mesh
type Triangle struct {
Vertices [9]float32 // 3 vertices * 3 components (x,y,z)
Normal [3]float32 // Face normal
GridID uint32 // Associated grid ID
WidgetID uint32 // Associated widget ID
BoundMin [3]float32 // Triangle bounding box minimum
BoundMax [3]float32 // Triangle bounding box maximum
}
// AABBNode represents a node in the Axis-Aligned Bounding Box tree
type AABBNode struct {
BoundMin [3]float32 // Node bounding box minimum
BoundMax [3]float32 // Node bounding box maximum
Left *AABBNode // Left child (nil for leaf nodes)
Right *AABBNode // Right child (nil for leaf nodes)
Triangles []*Triangle // Triangles (only for leaf nodes)
Depth uint32 // Tree depth
}
// RaycastResult contains the results of a raycast operation
type RaycastResult struct {
Hit bool // Whether the ray hit something
HitLocation [3]float32 // World coordinates of hit point
HitNormal [3]float32 // Surface normal at hit point
HitDistance float32 // Distance from ray origin to hit
GridID uint32 // Grid ID of hit triangle
WidgetID uint32 // Widget ID of hit triangle
}
// RaycastOptions configures raycast behavior
type RaycastOptions struct {
IgnoredWidgets map[uint32]bool // Widget IDs to ignore during raycast
MaxDistance float32 // Maximum ray distance (0 = unlimited)
BothSides bool // Check both sides of triangles
}
// NewRaycastMesh creates a new raycast mesh from triangle data
func NewRaycastMesh(vertices []float32, indices []uint32, grids []uint32, widgets []uint32,
maxDepth uint32, minLeafSize uint32, minAxisSize float32) (*RaycastMesh, error) {
if len(vertices)%3 != 0 {
return nil, fmt.Errorf("vertex count must be divisible by 3")
}
if len(indices)%3 != 0 {
return nil, fmt.Errorf("index count must be divisible by 3")
}
triangleCount := len(indices) / 3
if len(grids) != triangleCount {
return nil, fmt.Errorf("grid count must match triangle count")
}
if len(widgets) != triangleCount {
return nil, fmt.Errorf("widget count must match triangle count")
}
rm := &RaycastMesh{
vertices: make([]float32, len(vertices)),
indices: make([]uint32, len(indices)),
grids: make([]uint32, len(grids)),
widgets: make([]uint32, len(widgets)),
triangles: make([]*Triangle, triangleCount),
maxDepth: maxDepth,
minLeafSize: minLeafSize,
minAxisSize: minAxisSize,
}
// Copy input data
copy(rm.vertices, vertices)
copy(rm.indices, indices)
copy(rm.grids, grids)
copy(rm.widgets, widgets)
// Process triangles
if err := rm.processTriangles(); err != nil {
return nil, fmt.Errorf("failed to process triangles: %v", err)
}
// Build AABB tree
if err := rm.buildAABBTree(); err != nil {
return nil, fmt.Errorf("failed to build AABB tree: %v", err)
}
return rm, nil
}
// Raycast performs optimized raycasting using the AABB tree
func (rm *RaycastMesh) Raycast(from, to [3]float32, options *RaycastOptions) *RaycastResult {
rm.mutex.RLock()
defer rm.mutex.RUnlock()
if options == nil {
options = &RaycastOptions{}
}
result := &RaycastResult{
Hit: false,
HitDistance: math.MaxFloat32,
}
// Calculate ray direction and length
rayDir := [3]float32{
to[0] - from[0],
to[1] - from[1],
to[2] - from[2],
}
rayLength := vectorLength(rayDir)
if rayLength < 1e-6 {
return result // Zero-length ray
}
// Normalize ray direction
rayDir[0] /= rayLength
rayDir[1] /= rayLength
rayDir[2] /= rayLength
// Use max distance if specified
maxDist := rayLength
if options.MaxDistance > 0 && options.MaxDistance < rayLength {
maxDist = options.MaxDistance
}
// Traverse AABB tree
rm.raycastNode(rm.root, from, rayDir, maxDist, options, result)
return result
}
// BruteForceRaycast performs raycast without spatial optimization (for testing/comparison)
func (rm *RaycastMesh) BruteForceRaycast(from, to [3]float32, options *RaycastOptions) *RaycastResult {
rm.mutex.RLock()
defer rm.mutex.RUnlock()
if options == nil {
options = &RaycastOptions{}
}
result := &RaycastResult{
Hit: false,
HitDistance: math.MaxFloat32,
}
rayDir := [3]float32{
to[0] - from[0],
to[1] - from[1],
to[2] - from[2],
}
rayLength := vectorLength(rayDir)
if rayLength < 1e-6 {
return result
}
rayDir[0] /= rayLength
rayDir[1] /= rayLength
rayDir[2] /= rayLength
maxDist := rayLength
if options.MaxDistance > 0 && options.MaxDistance < rayLength {
maxDist = options.MaxDistance
}
// Test all triangles
for _, triangle := range rm.triangles {
if options.IgnoredWidgets != nil && options.IgnoredWidgets[triangle.WidgetID] {
continue
}
if hitDist, hitPoint, hitNormal := rm.rayTriangleIntersect(from, rayDir, triangle, options.BothSides); hitDist >= 0 && hitDist <= maxDist && hitDist < result.HitDistance {
result.Hit = true
result.HitDistance = hitDist
result.HitLocation = hitPoint
result.HitNormal = hitNormal
result.GridID = triangle.GridID
result.WidgetID = triangle.WidgetID
}
}
return result
}
// GetBoundMin returns the minimum bounding box coordinates
func (rm *RaycastMesh) GetBoundMin() [3]float32 {
rm.mutex.RLock()
defer rm.mutex.RUnlock()
return rm.boundMin
}
// GetBoundMax returns the maximum bounding box coordinates
func (rm *RaycastMesh) GetBoundMax() [3]float32 {
rm.mutex.RLock()
defer rm.mutex.RUnlock()
return rm.boundMax
}
// GetTriangleCount returns the number of triangles in the mesh
func (rm *RaycastMesh) GetTriangleCount() int {
rm.mutex.RLock()
defer rm.mutex.RUnlock()
return len(rm.triangles)
}
// GetTreeDepth returns the actual depth of the AABB tree
func (rm *RaycastMesh) GetTreeDepth() uint32 {
rm.mutex.RLock()
defer rm.mutex.RUnlock()
if rm.root == nil {
return 0
}
return rm.getNodeDepth(rm.root)
}
// Private methods
func (rm *RaycastMesh) processTriangles() error {
rm.boundMin = [3]float32{math.MaxFloat32, math.MaxFloat32, math.MaxFloat32}
rm.boundMax = [3]float32{-math.MaxFloat32, -math.MaxFloat32, -math.MaxFloat32}
for i := 0; i < len(rm.indices); i += 3 {
triangle := &Triangle{
GridID: rm.grids[i/3],
WidgetID: rm.widgets[i/3],
}
// Get vertex indices
i1, i2, i3 := rm.indices[i], rm.indices[i+1], rm.indices[i+2]
// Validate indices
if i1*3+2 >= uint32(len(rm.vertices)) || i2*3+2 >= uint32(len(rm.vertices)) || i3*3+2 >= uint32(len(rm.vertices)) {
return fmt.Errorf("invalid vertex index in triangle %d", i/3)
}
// Copy vertex positions
copy(triangle.Vertices[0:3], rm.vertices[i1*3:i1*3+3])
copy(triangle.Vertices[3:6], rm.vertices[i2*3:i2*3+3])
copy(triangle.Vertices[6:9], rm.vertices[i3*3:i3*3+3])
// Calculate triangle bounding box
triangle.BoundMin = [3]float32{math.MaxFloat32, math.MaxFloat32, math.MaxFloat32}
triangle.BoundMax = [3]float32{-math.MaxFloat32, -math.MaxFloat32, -math.MaxFloat32}
for j := 0; j < 9; j += 3 {
for k := 0; k < 3; k++ {
if triangle.Vertices[j+k] < triangle.BoundMin[k] {
triangle.BoundMin[k] = triangle.Vertices[j+k]
}
if triangle.Vertices[j+k] > triangle.BoundMax[k] {
triangle.BoundMax[k] = triangle.Vertices[j+k]
}
}
}
// Update global bounding box
for k := 0; k < 3; k++ {
if triangle.BoundMin[k] < rm.boundMin[k] {
rm.boundMin[k] = triangle.BoundMin[k]
}
if triangle.BoundMax[k] > rm.boundMax[k] {
rm.boundMax[k] = triangle.BoundMax[k]
}
}
// Calculate face normal
rm.calculateTriangleNormal(triangle)
rm.triangles[i/3] = triangle
}
return nil
}
func (rm *RaycastMesh) calculateTriangleNormal(triangle *Triangle) {
// Calculate two edge vectors
edge1 := [3]float32{
triangle.Vertices[3] - triangle.Vertices[0],
triangle.Vertices[4] - triangle.Vertices[1],
triangle.Vertices[5] - triangle.Vertices[2],
}
edge2 := [3]float32{
triangle.Vertices[6] - triangle.Vertices[0],
triangle.Vertices[7] - triangle.Vertices[1],
triangle.Vertices[8] - triangle.Vertices[2],
}
// Cross product
triangle.Normal[0] = edge1[1]*edge2[2] - edge1[2]*edge2[1]
triangle.Normal[1] = edge1[2]*edge2[0] - edge1[0]*edge2[2]
triangle.Normal[2] = edge1[0]*edge2[1] - edge1[1]*edge2[0]
// Normalize
length := vectorLength(triangle.Normal)
if length > 1e-6 {
triangle.Normal[0] /= length
triangle.Normal[1] /= length
triangle.Normal[2] /= length
}
}
func (rm *RaycastMesh) buildAABBTree() error {
if len(rm.triangles) == 0 {
return fmt.Errorf("no triangles to build tree from")
}
// Create root node with all triangles
rm.root = &AABBNode{
BoundMin: rm.boundMin,
BoundMax: rm.boundMax,
Triangles: rm.triangles,
Depth: 0,
}
// Recursively subdivide
rm.subdivideNode(rm.root)
return nil
}
func (rm *RaycastMesh) subdivideNode(node *AABBNode) {
// Stop subdivision if we've reached limits
if node.Depth >= rm.maxDepth || uint32(len(node.Triangles)) <= rm.minLeafSize {
return
}
// Find longest axis
size := [3]float32{
node.BoundMax[0] - node.BoundMin[0],
node.BoundMax[1] - node.BoundMin[1],
node.BoundMax[2] - node.BoundMin[2],
}
axis := 0
if size[1] > size[axis] {
axis = 1
}
if size[2] > size[axis] {
axis = 2
}
// Stop if axis is too small
if size[axis] < rm.minAxisSize {
return
}
// Split at midpoint
split := node.BoundMin[axis] + size[axis]*0.5
// Partition triangles
var leftTriangles, rightTriangles []*Triangle
for _, triangle := range node.Triangles {
center := (triangle.BoundMin[axis] + triangle.BoundMax[axis]) * 0.5
if center < split {
leftTriangles = append(leftTriangles, triangle)
} else {
rightTriangles = append(rightTriangles, triangle)
}
}
// Make sure both sides have triangles
if len(leftTriangles) == 0 || len(rightTriangles) == 0 {
return
}
// Create child nodes
node.Left = &AABBNode{
Triangles: leftTriangles,
Depth: node.Depth + 1,
}
node.Right = &AABBNode{
Triangles: rightTriangles,
Depth: node.Depth + 1,
}
// Calculate child bounding boxes
rm.calculateNodeBounds(node.Left)
rm.calculateNodeBounds(node.Right)
// Clear triangles from internal node
node.Triangles = nil
// Recursively subdivide children
rm.subdivideNode(node.Left)
rm.subdivideNode(node.Right)
}
func (rm *RaycastMesh) calculateNodeBounds(node *AABBNode) {
if len(node.Triangles) == 0 {
return
}
node.BoundMin = [3]float32{math.MaxFloat32, math.MaxFloat32, math.MaxFloat32}
node.BoundMax = [3]float32{-math.MaxFloat32, -math.MaxFloat32, -math.MaxFloat32}
for _, triangle := range node.Triangles {
for k := 0; k < 3; k++ {
if triangle.BoundMin[k] < node.BoundMin[k] {
node.BoundMin[k] = triangle.BoundMin[k]
}
if triangle.BoundMax[k] > node.BoundMax[k] {
node.BoundMax[k] = triangle.BoundMax[k]
}
}
}
}
func (rm *RaycastMesh) raycastNode(node *AABBNode, rayOrigin, rayDir [3]float32, maxDist float32,
options *RaycastOptions, result *RaycastResult) {
if node == nil {
return
}
// Test ray against node bounding box
if !rm.rayAABBIntersect(rayOrigin, rayDir, node.BoundMin, node.BoundMax, maxDist) {
return
}
// Leaf node - test triangles
if node.Left == nil && node.Right == nil {
for _, triangle := range node.Triangles {
if options.IgnoredWidgets != nil && options.IgnoredWidgets[triangle.WidgetID] {
continue
}
if hitDist, hitPoint, hitNormal := rm.rayTriangleIntersect(rayOrigin, rayDir, triangle, options.BothSides); hitDist >= 0 && hitDist <= maxDist && hitDist < result.HitDistance {
result.Hit = true
result.HitDistance = hitDist
result.HitLocation = hitPoint
result.HitNormal = hitNormal
result.GridID = triangle.GridID
result.WidgetID = triangle.WidgetID
}
}
} else {
// Internal node - recurse to children
rm.raycastNode(node.Left, rayOrigin, rayDir, maxDist, options, result)
rm.raycastNode(node.Right, rayOrigin, rayDir, maxDist, options, result)
}
}
func (rm *RaycastMesh) rayAABBIntersect(rayOrigin, rayDir [3]float32, boundMin, boundMax [3]float32, maxDist float32) bool {
var tMin, tMax float32 = 0, maxDist
for i := 0; i < 3; i++ {
if math.Abs(float64(rayDir[i])) < 1e-6 {
// Ray is parallel to axis
if rayOrigin[i] < boundMin[i] || rayOrigin[i] > boundMax[i] {
return false
}
} else {
// Calculate intersection distances
invDir := 1.0 / rayDir[i]
t1 := (boundMin[i] - rayOrigin[i]) * invDir
t2 := (boundMax[i] - rayOrigin[i]) * invDir
if t1 > t2 {
t1, t2 = t2, t1
}
tMin = float32(math.Max(float64(tMin), float64(t1)))
tMax = float32(math.Min(float64(tMax), float64(t2)))
if tMin > tMax {
return false
}
}
}
return tMin <= maxDist
}
func (rm *RaycastMesh) rayTriangleIntersect(rayOrigin, rayDir [3]float32, triangle *Triangle, bothSides bool) (float32, [3]float32, [3]float32) {
// Möller-Trumbore ray-triangle intersection algorithm
// Get triangle vertices
v0 := [3]float32{triangle.Vertices[0], triangle.Vertices[1], triangle.Vertices[2]}
v1 := [3]float32{triangle.Vertices[3], triangle.Vertices[4], triangle.Vertices[5]}
v2 := [3]float32{triangle.Vertices[6], triangle.Vertices[7], triangle.Vertices[8]}
// Edge vectors
edge1 := [3]float32{v1[0] - v0[0], v1[1] - v0[1], v1[2] - v0[2]}
edge2 := [3]float32{v2[0] - v0[0], v2[1] - v0[1], v2[2] - v0[2]}
// Cross product of ray direction and edge2
h := [3]float32{
rayDir[1]*edge2[2] - rayDir[2]*edge2[1],
rayDir[2]*edge2[0] - rayDir[0]*edge2[2],
rayDir[0]*edge2[1] - rayDir[1]*edge2[0],
}
// Dot product of edge1 and h
a := edge1[0]*h[0] + edge1[1]*h[1] + edge1[2]*h[2]
if math.Abs(float64(a)) < 1e-6 {
return -1, [3]float32{}, [3]float32{} // Ray is parallel to triangle
}
f := 1.0 / a
s := [3]float32{rayOrigin[0] - v0[0], rayOrigin[1] - v0[1], rayOrigin[2] - v0[2]}
u := f * (s[0]*h[0] + s[1]*h[1] + s[2]*h[2])
if u < 0.0 || u > 1.0 {
return -1, [3]float32{}, [3]float32{}
}
q := [3]float32{
s[1]*edge1[2] - s[2]*edge1[1],
s[2]*edge1[0] - s[0]*edge1[2],
s[0]*edge1[1] - s[1]*edge1[0],
}
v := f * (rayDir[0]*q[0] + rayDir[1]*q[1] + rayDir[2]*q[2])
if v < 0.0 || u+v > 1.0 {
return -1, [3]float32{}, [3]float32{}
}
t := f * (edge2[0]*q[0] + edge2[1]*q[1] + edge2[2]*q[2])
if t < 1e-6 { // Ray intersection behind origin
return -1, [3]float32{}, [3]float32{}
}
// Check backface culling
if !bothSides && a > 0 {
return -1, [3]float32{}, [3]float32{}
}
// Calculate hit point
hitPoint := [3]float32{
rayOrigin[0] + rayDir[0]*t,
rayOrigin[1] + rayDir[1]*t,
rayOrigin[2] + rayDir[2]*t,
}
// Use precomputed normal
hitNormal := triangle.Normal
return t, hitPoint, hitNormal
}
func (rm *RaycastMesh) getNodeDepth(node *AABBNode) uint32 {
if node == nil {
return 0
}
if node.Left == nil && node.Right == nil {
return node.Depth
}
leftDepth := rm.getNodeDepth(node.Left)
rightDepth := rm.getNodeDepth(node.Right)
if leftDepth > rightDepth {
return leftDepth
}
return rightDepth
}
// Utility functions
func vectorLength(v [3]float32) float32 {
return float32(math.Sqrt(float64(v[0]*v[0] + v[1]*v[1] + v[2]*v[2])))
}