Files
ngzz-mc/internal/raycast/raycast.go

75 lines
2.3 KiB
Go
Raw Normal View History

// Package raycast 体素射线拾取:DDA 步进(场景/方块交互与动画.md §2)。
//
// 返回三元组:hitBlock(被击中方块)、faceNormal(击中面法线)、prevBlock(放置位)。
// 客户端与服务器共用同一实现,保证双端判定一致。
package raycast
import "math"
// Hit 射线命中结果。
type Hit struct {
X, Y, Z int32 // 被击中的方块坐标
Face [3]int32 // 击中面法线(±1 单位向量)
Prev [3]int32 // 放置位 = 被击中方块 - 面法线
Dist float64 // 命中距离
}
// Solid 方块可交互判定回调(用注册表 IsSolid 等)。
type Solid func(x, y, z int32) bool
// Cast 从 origin 沿 dir 做 DDA 体素步进,最大距离 maxDist,返回首个 solid 命中。
// dir 应为单位向量;分量接近 0 时对应轴 tDelta 取 +Inf(不会在该轴步进)。
func Cast(origin, dir [3]float64, maxDist float64, solid Solid) (Hit, bool) {
x, y, z := int32(math.Floor(origin[0])), int32(math.Floor(origin[1])), int32(math.Floor(origin[2]))
// 基准取整值(负数安全:不能用 int32() 截断,-0.5 会被截成 0)
fx, fy, fz := math.Floor(origin[0]), math.Floor(origin[1]), math.Floor(origin[2])
var step [3]int32
var tDelta, tMax [3]float64
for i := 0; i < 3; i++ {
base := [3]float64{fx, fy, fz}[i]
switch {
case dir[i] > 0:
step[i] = 1
tDelta[i] = 1 / dir[i]
tMax[i] = (base + 1 - origin[i]) / dir[i]
case dir[i] < 0:
step[i] = -1
tDelta[i] = -1 / dir[i]
tMax[i] = (origin[i] - base) / (-dir[i])
default:
step[i] = 0
tDelta[i] = math.Inf(1)
tMax[i] = math.Inf(1)
}
}
var face [3]int32
dist := 0.0
for dist <= maxDist {
if solid(x, y, z) {
// 当前格命中:面法线指向射线来向(背离方块),放置位 = 命中格 + 面法线
return Hit{X: x, Y: y, Z: z, Face: face, Prev: [3]int32{x + face[0], y + face[1], z + face[2]}, Dist: dist}, true
}
// 选择 tMax 最小的轴步进
switch {
case tMax[0] < tMax[1] && tMax[0] < tMax[2]:
dist = tMax[0]
x += step[0]
tMax[0] += tDelta[0]
face = [3]int32{-step[0], 0, 0}
case tMax[1] < tMax[2]:
dist = tMax[1]
y += step[1]
tMax[1] += tDelta[1]
face = [3]int32{0, -step[1], 0}
default:
dist = tMax[2]
z += step[2]
tMax[2] += tDelta[2]
face = [3]int32{0, 0, -step[2]}
}
}
return Hit{}, false
}