Files
ngzz-mc/internal/mesh/mesh_test.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

149 lines
4.6 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
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))
}
}