Files
ngzz-mc/internal/mesh/mesh.go
NianGao Dev 9e2f5a6186 fix(game): 崩溃修复(卸载区块实体冻结)+ 立面光照、掉落可见、打击感、水下效果、空手拳头、血条左/饥饿右
- 崩溃(index out of range):实体所在区块卸载后失去碰撞支撑无限下坠到
  负坐标,光查询越界 panic——WorldView 增 ChunkLoaded,未加载区块实体冻结;
  chunk 光查询加越界防御(双保险)
- 立面光照:mesh 每面取**邻格**光照(原版光照模型)——此前用方块自身光照
  (sky=0)导致悬崖/矿洞立面全黑(泥土竖向黑色根因)
- 掉落可见:生成后 1 秒拾取延迟 + 图标加大到 0.45
- 打击感:攻击命中音(block.hit)+ 实体受击白闪(uTint)+ 手持挥动动画
- 水下效果:相机入水蓝色叠加(渲染.md §8 水体)
- 空手显示肤色拳头(白羊毛 × 肤色 tint),持方块正常显示
- HUD 布局:血条左下、饥饿右下(经典布局)
2026-08-16 20:59:28 +08:00

185 lines
6.5 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Package mesh 区块网格构建:面剔除 + 顶点格式 + 光照(场景/渲染.md §3)。
//
// 网格在 CPU 构建(worker 池),GL 只负责上传——渲染 goroutine 不产生分配压力。
package mesh
import (
"sync"
"mc/internal/block"
"mc/internal/chunk"
)
// vertexPools 顶点切片池(性能与内存.md §2:热路径零分配——渲染线程不造 mesh 垃圾)。
var vertexPools = [3]sync.Pool{
{New: func() any { s := make([]Vertex, 0, 4096); return &s }},
{New: func() any { s := make([]Vertex, 0, 1024); return &s }},
{New: func() any { s := make([]Vertex, 0, 1024); return &s }},
}
// Release 归还网格顶点缓冲到池(GPU 上传后调用,性能与内存.md §8 sync.Pool 复位)。
func Release(m *ChunkMesh) {
if m == nil {
return
}
for i, s := range [3]*[]Vertex{&m.Opaque, &m.Cutout, &m.Translucent} {
if cap(*s) > 0 && cap(*s) <= 1<<20 {
vertexPools[i].Put(s)
}
*s = nil
}
}
// Vertex 顶点格式(渲染.md §5):位置 + UV + 天空/方块光各 4-bit。
type Vertex struct {
X, Y, Z float32
U, V float32
Sky, Block uint8
}
// ChunkMesh 一个区块的三类网格(渲染.md §6 三 pass)。
// 每面 4 顶点(四边形),渲染层用索引缓冲扩为 2 三角形(顺序 0-1-2-2-3-0)。
type ChunkMesh struct {
Opaque []Vertex // 不透明(主 pass)
Cutout []Vertex // alpha test(树叶/玻璃)
Translucent []Vertex // 半透明(水,远→近排序 pass)
}
// Neighbor 邻居方块查询(跨区块面剔除用,区块管理.md §6)。
type Neighbor func(x, y, z int32) block.State
// Light 邻居光照查询(面光照用:每个面取**邻格**的光照等级——
// 方块自身 sky=0,用自身光照会让所有立面全黑(悬崖/矿洞立面黑色根因)。
type Light func(x, y, z int32) (sky, blk uint8)
// UVFunc 纹理键 → 图集 UV(归一化坐标,含 inset)。
type UVFunc func(texKey string) (u0, v0, u1, v1 float32, ok bool)
// Builder 网格构建器。
type Builder struct {
reg *block.Registry
uv UVFunc
light Light // 面光照查询(邻格光照;nil 回退自身光照)
}
// NewBuilder 创建构建器。
func NewBuilder(reg *block.Registry, uv UVFunc) *Builder {
return &Builder{reg: reg, uv: uv}
}
// SetLight 设置面光照查询(每个面取邻格光照,光照.md 顶点光照)。
func (b *Builder) SetLight(fn Light) { b.light = fn }
// face 一个面的 4 个角偏移与朝向。
type faceDef struct {
dir [3]int32
corners [4][3]int32
}
// faces 六面定义(角顺序与 UV 顶点顺序一致)。
// 绕序铁律:所有面必须为「窗口空间顺时针」(FrontFace=CW 的正面)——
// 旧定义只有顶面是 CW,垂直面与底面是 CCW 被背面剔除,
// 表现为「立面透明的空洞、移动时面闪烁」(方格立面缺失根因)。
var faces = [6]faceDef{
{dir: [3]int32{0, 1, 0}, corners: [4][3]int32{{0, 1, 0}, {1, 1, 0}, {1, 1, 1}, {0, 1, 1}}}, // 顶
{dir: [3]int32{0, -1, 0}, corners: [4][3]int32{{0, 0, 0}, {1, 0, 0}, {1, 0, 1}, {0, 0, 1}}}, // 底(反向)
{dir: [3]int32{1, 0, 0}, corners: [4][3]int32{{1, 1, 0}, {1, 1, 1}, {1, 0, 1}, {1, 0, 0}}}, // 东(+x)(反向)
{dir: [3]int32{-1, 0, 0}, corners: [4][3]int32{{0, 1, 1}, {0, 1, 0}, {0, 0, 0}, {0, 0, 1}}}, // 西(-x)(反向)
{dir: [3]int32{0, 0, 1}, corners: [4][3]int32{{1, 1, 1}, {0, 1, 1}, {0, 0, 1}, {1, 0, 1}}}, // 南(+z)(反向)
{dir: [3]int32{0, 0, -1}, corners: [4][3]int32{{0, 1, 0}, {1, 1, 0}, {1, 0, 0}, {0, 0, 0}}}, // 北(-z)(反向)
}
// faceTexture 面方向 → 注册表纹理面名(block.Registry.Texture 参数)。
var faceTexture = [6]string{"up", "down", "east", "west", "south", "north"}
// Build 构建区块网格(面剔除:仅当邻格非完全不透明时生成该面,渲染.md §3.1)。
func (b *Builder) Build(c *chunk.Chunk, nb Neighbor) *ChunkMesh {
m := &ChunkMesh{}
// 顶点缓冲从池中取(性能与内存.md §2)
ob := vertexPools[0].Get().(*[]Vertex)
cb := vertexPools[1].Get().(*[]Vertex)
tb := vertexPools[2].Get().(*[]Vertex)
*ob, *cb, *tb = (*ob)[:0], (*cb)[:0], (*tb)[:0]
blocks := c.Snapshot()
baseX, baseZ := float32(c.CX*16), float32(c.CZ*16)
for x := 0; x < 16; x++ {
for y := 0; y < chunk.Height; y++ {
for z := 0; z < 16; z++ {
s := blocks[chunk.Index(x, y, z)]
if s == block.Air {
continue
}
var target *[]Vertex
switch {
case b.reg.IsTranslucent(s):
target = tb
case b.reg.IsCutout(s):
target = cb
default:
target = ob
}
wx := int32(x) + c.CX*16
wy := int32(y)
wz := int32(z) + c.CZ*16
for fi, f := range faces {
nx := wx + f.dir[0]
ny := wy + f.dir[1]
nz := wz + f.dir[2]
neighbor := nb(nx, ny, nz)
if b.reg.IsOpaque(neighbor) {
continue // 面被不透明邻格遮挡(面剔除)
}
// 同类型邻格也剔除(水贴水不渲染内面;树叶相邻保留边缘,简化规则)
if neighbor != block.Air && !b.reg.IsOpaque(neighbor) &&
b.reg.IsTranslucent(neighbor) == b.reg.IsTranslucent(s) && b.reg.IsTranslucent(s) {
continue
}
// 面光照:取**邻格**的光照(原版光照模型)——方块自身 sky=0,
// 用自身光照会让悬崖/矿洞立面全黑
sky, blk := uint8(0), uint8(0)
if b.light != nil {
sky, blk = b.light(nx, ny, nz)
} else {
sky, blk = c.SkyLight(x, y, z), c.BlockLight(x, y, z)
}
texKey := b.reg.Texture(s, faceTexture[fi])
u0, v0, u1, v1, ok := b.uv(texKey)
if !ok {
u0, v0, u1, v1 = 0, 0, 1, 1
}
for _, corner := range f.corners {
*target = append(*target, Vertex{
X: baseX + float32(x) + float32(corner[0]),
Y: float32(y) + float32(corner[1]),
Z: baseZ + float32(z) + float32(corner[2]),
U: u0 + (u1-u0)*float32(cornerUV(corner, f)[0]),
V: v0 + (v1-v0)*float32(cornerUV(corner, f)[1]),
Sky: sky,
Block: blk,
})
}
}
}
}
}
// 挂回结果(注意:切片头来自池,调用方上传后必须 Release)
m.Opaque, m.Cutout, m.Translucent = *ob, *cb, *tb
return m
}
// cornerUV 角在面上的 UV 权重(0/1),顺序与 corners 一致。
func cornerUV(c [3]int32, f faceDef) [2]int32 {
var u, v int32
switch f.dir {
case [3]int32{0, 1, 0}, [3]int32{0, -1, 0}: // 顶/底:x→u,z→v
u, v = c[0], c[2]
case [3]int32{1, 0, 0}, [3]int32{-1, 0, 0}: // 东/西:z→u,y→v
u, v = c[2], 1-c[1]
default: // 南/北:x→u,y→v
u, v = c[0], 1-c[1]
}
return [2]int32{u, v}
}