// 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 }