Files
ngzz-mc/internal/mesh/mesh.go
NianGao Dev 95bd510bf8 fix(render): 垂直面渲染恢复——绕序改回窗口空间 CW + 侧面纹理键回退
- 六面绕序按 GL 窗口坐标(x 右、y 上)验证:曾按 y 向下约定反转五面,
  垂直面全部变成背面被剔除(只有顶面渲染、透空见区块断面)
- mesh.TestFaceWinding 鞋带公式回归测试锁死绕序约定
- Registry.Texture 对 east/west/north 回退 side、south 回退 front/side
  (原版 blocks.json 惯例),立面不再落到不存在的 block/<name> 灰块
- World.SetBlock 破坏/放置空气时按列重算天空光(洞壁不再全黑)
2026-08-16 22:32:07 +08:00

187 lines
6.7 KiB
Go
Raw Permalink 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 的正面)——
// 窗口坐标 x 向右、y 向上(GL 约定,非图像 y 向下),
// 用鞋带公式(y 向上)验证:从面外侧看向方块,area < 0 才是 CW。
// 曾按 y 向下的约定反转五面,导致垂直面全部变成背面被剔除
// (表现为「只有顶面渲染」,世界透空看到区块断面——立面缺失根因)。
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, 1}, {1, 0, 1}, {1, 0, 0}, {0, 0, 0}}}, // 底
{dir: [3]int32{1, 0, 0}, corners: [4][3]int32{{1, 0, 0}, {1, 0, 1}, {1, 1, 1}, {1, 1, 0}}}, // 东(+x)
{dir: [3]int32{-1, 0, 0}, corners: [4][3]int32{{0, 0, 1}, {0, 0, 0}, {0, 1, 0}, {0, 1, 1}}}, // 西(-x)
{dir: [3]int32{0, 0, 1}, corners: [4][3]int32{{1, 0, 1}, {0, 0, 1}, {0, 1, 1}, {1, 1, 1}}}, // 南(+z)
{dir: [3]int32{0, 0, -1}, corners: [4][3]int32{{0, 0, 0}, {1, 0, 0}, {1, 1, 0}, {0, 1, 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}
}