fix(render): 垂直面渲染恢复——绕序改回窗口空间 CW + 侧面纹理键回退
- 六面绕序按 GL 窗口坐标(x 右、y 上)验证:曾按 y 向下约定反转五面, 垂直面全部变成背面被剔除(只有顶面渲染、透空见区块断面) - mesh.TestFaceWinding 鞋带公式回归测试锁死绕序约定 - Registry.Texture 对 east/west/north 回退 side、south 回退 front/side (原版 blocks.json 惯例),立面不再落到不存在的 block/<name> 灰块 - World.SetBlock 破坏/放置空气时按列重算天空光(洞壁不再全黑)
This commit is contained in:
@@ -135,11 +135,27 @@ func (r *Registry) IsTranslucent(s State) bool { return r.Get(s.ID()).Translucen
|
||||
func (r *Registry) IsFluid(s State) bool { return r.Get(s.ID()).Fluid }
|
||||
|
||||
// Texture 取方块某个面的纹理键(如 "up"/"side"/"all")。
|
||||
// 原版 blocks.json 惯例:四个侧面统一用 "side"(南面可用 "front" 覆盖,如熔炉)。
|
||||
// mesh 层传入 east/west/south/north 方位名,这里回退到 side/front,
|
||||
// 否则 grass_block 等立面会落到 "block/<name>"(图集中不存在)→ 全图集灰块。
|
||||
func (r *Registry) Texture(s State, face string) string {
|
||||
d := r.Get(s.ID())
|
||||
if t, ok := d.Textures[face]; ok {
|
||||
return t
|
||||
}
|
||||
switch face {
|
||||
case "south": // 南面优先 front(熔炉门面),其次 side
|
||||
if t, ok := d.Textures["front"]; ok {
|
||||
return t
|
||||
}
|
||||
if t, ok := d.Textures["side"]; ok {
|
||||
return t
|
||||
}
|
||||
case "east", "west", "north":
|
||||
if t, ok := d.Textures["side"]; ok {
|
||||
return t
|
||||
}
|
||||
}
|
||||
if t, ok := d.Textures["all"]; ok {
|
||||
return t
|
||||
}
|
||||
|
||||
@@ -78,15 +78,17 @@ type faceDef struct {
|
||||
|
||||
// faces 六面定义(角顺序与 UV 顶点顺序一致)。
|
||||
// 绕序铁律:所有面必须为「窗口空间顺时针」(FrontFace=CW 的正面)——
|
||||
// 旧定义只有顶面是 CW,垂直面与底面是 CCW 被背面剔除,
|
||||
// 表现为「立面透明的空洞、移动时面闪烁」(方格立面缺失根因)。
|
||||
// 窗口坐标 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, 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)(反向)
|
||||
{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 参数)。
|
||||
|
||||
@@ -88,6 +88,43 @@ func TestFaceCulling(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
|
||||
@@ -15,14 +15,14 @@ import (
|
||||
)
|
||||
|
||||
// cubeFaces 单位立方体 6 面角顺序(与 mesh.faceDef 一致,
|
||||
// 窗口空间 CW 绕序为 FrontFace(CW) 的正面)。
|
||||
// 窗口空间 CW 绕序为 FrontFace(CW) 的正面;窗口坐标 y 向上)。
|
||||
var cubeFaces = [6][4][3]float32{
|
||||
{{0, 1, 0}, {1, 1, 0}, {1, 1, 1}, {0, 1, 1}}, // 顶
|
||||
{{0, 0, 0}, {1, 0, 0}, {1, 0, 1}, {0, 0, 1}}, // 底(反向)
|
||||
{{1, 1, 0}, {1, 1, 1}, {1, 0, 1}, {1, 0, 0}}, // 东(+x)(反向)
|
||||
{{0, 1, 1}, {0, 1, 0}, {0, 0, 0}, {0, 0, 1}}, // 西(-x)(反向)
|
||||
{{1, 1, 1}, {0, 1, 1}, {0, 0, 1}, {1, 0, 1}}, // 南(+z)(反向)
|
||||
{{0, 1, 0}, {1, 1, 0}, {1, 0, 0}, {0, 0, 0}}, // 北(-z)(反向)
|
||||
{{0, 0, 1}, {1, 0, 1}, {1, 0, 0}, {0, 0, 0}}, // 底
|
||||
{{1, 0, 0}, {1, 0, 1}, {1, 1, 1}, {1, 1, 0}}, // 东(+x)
|
||||
{{0, 0, 1}, {0, 0, 0}, {0, 1, 0}, {0, 1, 1}}, // 西(-x)
|
||||
{{1, 0, 1}, {0, 0, 1}, {0, 1, 1}, {1, 1, 1}}, // 南(+z)
|
||||
{{0, 0, 0}, {1, 0, 0}, {1, 1, 0}, {0, 1, 0}}, // 北(-z)
|
||||
}
|
||||
|
||||
// cubeVerts 按六面 UV 构建单位立方体顶点(mesh.Vertex 格式:pos+uv+light 字节,
|
||||
|
||||
@@ -250,6 +250,12 @@ func (w *World) SetBlock(x, y, z int32, s block.State) {
|
||||
prev := ch.Block(lx, int(y), lz)
|
||||
ch.SetBlock(lx, int(y), lz, s)
|
||||
|
||||
// 天空光按列重算:破坏方块后洞内空气获得天光(否则挖出的洞壁全黑,
|
||||
// 光照.md §3 列更新);放置方块后下方渐变变暗(原版行为)
|
||||
if s == block.Air || prev == block.Air {
|
||||
light.SkyFill(w, x, x, z, z)
|
||||
}
|
||||
|
||||
prevLight := w.reg.Light(prev)
|
||||
newLight := w.reg.Light(s)
|
||||
switch {
|
||||
|
||||
@@ -29,6 +29,8 @@ func main() {
|
||||
|
||||
- 只为「邻格是空气/透明」的面生成顶点,被完全不透明方块包裹的面直接丢弃(设计.md §5.1)。
|
||||
- 这一条不做,draw call 和顶点数直接爆炸。
|
||||
- **绕序铁律(GL 3.3 core)**:`gl.FrontFace(gl.CW)` 下,六面顶点序必须是**窗口空间顺时针**——窗口坐标 **x 向右、y 向上**(GL 约定,不是图像坐标 y 向下)。用鞋带公式验证:从面外侧看向方块,有向面积 **< 0 才是 CW**。曾按 y 向下约定把五面反转,导致垂直面全部变成背面被剔除——世界只剩顶面、透空看到区块断面("只有顶部渲染"根因)。回归测试:`mesh.TestFaceWinding`。
|
||||
- **纹理键约定(原版 blocks.json)**:四个侧面统一用 `"side"`(南面可被 `"front"` 覆盖,如熔炉),`mesh` 层传 `east/west/south/north` 方位名,注册表 `Texture()` 负责回退到 `side/front/all`,否则立面会落到 `block/<name>`(图集中不存在)→ 整图集平均灰块。
|
||||
|
||||
### 3.2 第二步:Greedy Meshing(分水岭)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user