- 六面绕序按 GL 窗口坐标(x 右、y 上)验证:曾按 y 向下约定反转五面, 垂直面全部变成背面被剔除(只有顶面渲染、透空见区块断面) - mesh.TestFaceWinding 鞋带公式回归测试锁死绕序约定 - Registry.Texture 对 east/west/north 回退 side、south 回退 front/side (原版 blocks.json 惯例),立面不再落到不存在的 block/<name> 灰块 - World.SetBlock 破坏/放置空气时按列重算天空光(洞壁不再全黑)
149 lines
4.6 KiB
Go
149 lines
4.6 KiB
Go
package mesh
|
||
|
||
import (
|
||
"path/filepath"
|
||
"testing"
|
||
|
||
"mc/internal/block"
|
||
"mc/internal/chunk"
|
||
)
|
||
|
||
// loadReg 加载注册表。
|
||
func loadReg(t *testing.T) *block.Registry {
|
||
t.Helper()
|
||
r, err := block.Load(filepath.Join("..", "..", "assets", "config", "blocks.json"))
|
||
if err != nil {
|
||
t.Fatalf("加载注册表失败: %v", err)
|
||
}
|
||
return r
|
||
}
|
||
|
||
// airNb 邻居查询:全部视为空气(不剔除任何面)。
|
||
func airNb(x, y, z int32) block.State { return block.Air }
|
||
|
||
// dummyUV 固定 UV(测试不关心贴图坐标)。
|
||
func dummyUV(key string) (float32, float32, float32, float32, bool) {
|
||
return 0, 0, 1, 1, true
|
||
}
|
||
|
||
// TestPoolReuse 顶点池复用:Build → Release → 再次 Build 不崩溃且结果正确(性能与内存.md §2)。
|
||
func TestPoolReuse(t *testing.T) {
|
||
reg := loadReg(t)
|
||
stone, _ := reg.ID("stone")
|
||
c := chunk.New(0, 0)
|
||
c.Fill(0, 0, 0, block.NewState(stone, 0))
|
||
b := NewBuilder(reg, dummyUV)
|
||
for i := 0; i < 3; i++ {
|
||
m := b.Build(c, airNb)
|
||
if len(m.Opaque) != 24 {
|
||
t.Fatalf("第 %d 次构建顶点数异常: %d", i, len(m.Opaque))
|
||
}
|
||
Release(m)
|
||
}
|
||
}
|
||
|
||
// TestSingleBlock 单方块 6 面全生成:24 顶点(6 面 × 4 顶点四边形)。
|
||
func TestSingleBlock(t *testing.T) {
|
||
reg := loadReg(t)
|
||
stone, _ := reg.ID("stone")
|
||
c := chunk.New(0, 0)
|
||
c.Fill(0, 0, 0, block.NewState(stone, 0))
|
||
c.SetBlockLight(0, 0, 0, 10)
|
||
|
||
b := NewBuilder(reg, dummyUV)
|
||
m := b.Build(c, airNb)
|
||
if len(m.Opaque) != 24 {
|
||
t.Fatalf("不透明顶点期望 24,实际 %d", len(m.Opaque))
|
||
}
|
||
if len(m.Cutout) != 0 || len(m.Translucent) != 0 {
|
||
t.Fatalf("其他通道应为空: cutout=%d translucent=%d", len(m.Cutout), len(m.Translucent))
|
||
}
|
||
// 顶点光照传递
|
||
for _, v := range m.Opaque {
|
||
if v.Block != 10 {
|
||
t.Fatalf("顶点方块光期望 10,实际 %d", v.Block)
|
||
}
|
||
}
|
||
}
|
||
|
||
// TestFaceCulling 相邻不透明方块剔除共享面:2 块相邻 → 各 5 面 → 40 顶点。
|
||
func TestFaceCulling(t *testing.T) {
|
||
reg := loadReg(t)
|
||
stone, _ := reg.ID("stone")
|
||
c := chunk.New(0, 0)
|
||
c.Fill(0, 0, 0, block.NewState(stone, 0))
|
||
c.Fill(1, 0, 0, block.NewState(stone, 0))
|
||
|
||
// 邻居查询:跨区块查空气,区块内查实际方块
|
||
nb := func(x, y, z int32) block.State {
|
||
if x < 0 || x >= 16 || y < 0 || y >= chunk.Height || z < 0 || z >= 16 {
|
||
return block.Air
|
||
}
|
||
return c.Block(int(x), int(y), int(z))
|
||
}
|
||
b := NewBuilder(reg, dummyUV)
|
||
m := b.Build(c, nb)
|
||
if len(m.Opaque) != 40 {
|
||
t.Fatalf("面剔除后顶点期望 40,实际 %d", len(m.Opaque))
|
||
}
|
||
}
|
||
|
||
// TestFaceWinding 六面绕序:从面外侧看必须为窗口空间顺时针(FrontFace=CW 的正面)。
|
||
// 窗口坐标 x 向右、y 向上(GL 约定)——鞋带公式面积 < 0 为 CW。
|
||
// 回归:曾按 y 向下约定反转五面,垂直面全被背面剔除("只有顶面渲染")。
|
||
func TestFaceWinding(t *testing.T) {
|
||
// 各面外侧视角的相机基向量(right/up),用于把角投影到窗口 2D 坐标
|
||
view := map[[3]int32][2][3]float32{
|
||
{0, 1, 0}: {{1, 0, 0}, {0, 0, -1}}, // 顶:从 +y 俯视
|
||
{0, -1, 0}: {{1, 0, 0}, {0, 0, 1}}, // 底:从 -y 仰视
|
||
{1, 0, 0}: {{0, 0, -1}, {0, 1, 0}}, // 东:从 +x 看
|
||
{-1, 0, 0}: {{0, 0, 1}, {0, 1, 0}}, // 西:从 -x 看
|
||
{0, 0, 1}: {{1, 0, 0}, {0, 1, 0}}, // 南:从 +z 看
|
||
{0, 0, -1}: {{-1, 0, 0}, {0, 1, 0}}, // 北:从 -z 看
|
||
}
|
||
for _, f := range faces {
|
||
basis, ok := view[f.dir]
|
||
if !ok {
|
||
t.Fatalf("面 %v 缺少视角定义", f.dir)
|
||
}
|
||
right, up := basis[0], basis[1]
|
||
var px, py [4]float32
|
||
for i, c := range f.corners {
|
||
w := [3]float32{float32(c[0]), float32(c[1]), float32(c[2])}
|
||
px[i] = w[0]*right[0] + w[1]*right[1] + w[2]*right[2]
|
||
py[i] = w[0]*up[0] + w[1]*up[1] + w[2]*up[2]
|
||
}
|
||
// 鞋带公式(y 向上:面积 > 0 为 CCW,< 0 为 CW)
|
||
area := float32(0)
|
||
for i := 0; i < 4; i++ {
|
||
j := (i + 1) % 4
|
||
area += px[i]*py[j] - px[j]*py[i]
|
||
}
|
||
if area >= 0 {
|
||
t.Fatalf("面 %v 绕序错误:面积 %.1f >= 0(应为 CW,面积 < 0),会被背面剔除", f.dir, area)
|
||
}
|
||
}
|
||
}
|
||
|
||
// TestChannels 透明分类:火把→cutout,水→translucent(渲染.md §6)。
|
||
func TestChannels(t *testing.T) {
|
||
reg := loadReg(t)
|
||
torch, _ := reg.ID("torch")
|
||
water, _ := reg.ID("water")
|
||
c := chunk.New(0, 0)
|
||
c.Fill(0, 0, 0, block.NewState(torch, 0))
|
||
c.Fill(2, 0, 0, block.NewState(water, 0))
|
||
|
||
b := NewBuilder(reg, dummyUV)
|
||
m := b.Build(c, airNb)
|
||
if len(m.Cutout) != 24 {
|
||
t.Fatalf("火把 cutout 顶点期望 24,实际 %d", len(m.Cutout))
|
||
}
|
||
if len(m.Translucent) != 24 {
|
||
t.Fatalf("水 translucent 顶点期望 24,实际 %d", len(m.Translucent))
|
||
}
|
||
if len(m.Opaque) != 0 {
|
||
t.Fatalf("不应有不透明顶点,实际 %d", len(m.Opaque))
|
||
}
|
||
}
|