fix(render): 修复白屏/黑屏——视图矩阵、UI着色器、网格构建性能与安装目录
- 相机视图矩阵按 R_cam^T·T(-pos) 正确推导(旧布局非正交,非零视角下场景塌缩为灰屏) - UI 片元着色器改为纹理×颜色(旧实现丢弃纹理 RGB,彩色纹理全白) - UI 纹理与图集 mipmap:图集补齐 2 次幂、UI 上传不生成 mipmap(NPOT 不完整纹理采样纯白) - 网格构建持一次读锁(旧实现每格邻居查询各加锁约 40 万次/区块,出生区块迟迟无网格, 玩家透过脚下的洞看到地下横截面——进游戏黑屏主因);脏区块按距离排序优先构建 - 开启背面剔除(CW 正面):区块边界面向未加载邻居的内侧面不再以暗色出现在视野里 - 出生逻辑:等区块加载后定位地表出生,相机略微俯视 - dev 截图测试:-shot 等待玩家所在 5×5 区块网格就绪再拍(48s 兜底),先测试后打包 - 安装器改为按用户安装到 LOCALAPPDATA(Program Files 无写权限导致启动即崩) - dev 截图工具独立成 devshot_gl.go
This commit is contained in:
@@ -11,6 +11,7 @@ import (
|
||||
"flag"
|
||||
"os"
|
||||
"os/signal"
|
||||
"sort"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -18,6 +19,7 @@ import (
|
||||
"mc/internal/atlas"
|
||||
"mc/internal/block"
|
||||
"mc/internal/camera"
|
||||
"mc/internal/chunk"
|
||||
"mc/internal/config"
|
||||
"mc/internal/crash"
|
||||
"mc/internal/engine"
|
||||
@@ -41,6 +43,12 @@ import (
|
||||
|
||||
var devMode = flag.Bool("dev", devDefault, "开发模式:直接读取 assets/(默认随构建 tag)")
|
||||
|
||||
// 开发测试参数(dev 测试通过后再打包)
|
||||
var (
|
||||
autoEnter = flag.Bool("auto", false, "自动进入游戏(跳过主菜单点击)")
|
||||
shotPath = flag.String("shot", "", "截图路径:运行约 2 秒后保存 PNG 并退出(dev 测试用)")
|
||||
)
|
||||
|
||||
// clientApp 客户端应用:把会话与渲染粘合起来(engine.Ticker + engine.Renderer)。
|
||||
type clientApp struct {
|
||||
sess *game.Session
|
||||
@@ -70,6 +78,7 @@ type clientApp struct {
|
||||
|
||||
winW, winH int
|
||||
tickCnt uint64
|
||||
frames int // 渲染帧计数(截图时机用)
|
||||
lastTime time.Time
|
||||
}
|
||||
|
||||
@@ -84,6 +93,12 @@ func (a *clientApp) Tick(dt float64) {
|
||||
// 屏幕状态机(UI还原.md §2:主菜单/暂停不处理游戏输入)
|
||||
switch a.screen {
|
||||
case 0: // 主菜单
|
||||
// dev 测试:-auto 自动进入游戏(跳过点击)
|
||||
if *autoEnter {
|
||||
a.screen = 1
|
||||
a.rend.SetCursorMode(true)
|
||||
return
|
||||
}
|
||||
if a.in.clicked {
|
||||
switch a.titleScreen.HitTest(a.in.clickX, a.in.clickY) {
|
||||
case "play":
|
||||
@@ -157,22 +172,38 @@ func (a *clientApp) Tick(dt float64) {
|
||||
}
|
||||
}
|
||||
|
||||
// 脏区块网格重建(CPU 侧,预算内)
|
||||
// 脏区块网格重建(CPU 侧,预算内;按距玩家距离排序,先建视野中心——
|
||||
// 随机顺序会致出生区块迟迟无网格,玩家脚下的地面"透明",看到相邻区块地下横截面(黑)。
|
||||
start := time.Now()
|
||||
px, pz := a.sess.Player.Pos[0], a.sess.Player.Pos[2]
|
||||
type dirtyJob struct {
|
||||
c *chunk.Chunk
|
||||
d float64
|
||||
}
|
||||
var dirty []dirtyJob
|
||||
for _, c := range a.sess.World.ActiveChunks() {
|
||||
if time.Since(start) > 6*time.Millisecond {
|
||||
break // 帧预算(区块管理.md §4.3.2)
|
||||
}
|
||||
if !c.Dirty.Load() {
|
||||
continue
|
||||
}
|
||||
m := a.meshBuilder.Build(c, func(x, y, z int32) block.State {
|
||||
return a.sess.World.Block(x, y, z)
|
||||
dx := float64(c.CX)*16 + 8 - px
|
||||
dz := float64(c.CZ)*16 + 8 - pz
|
||||
dirty = append(dirty, dirtyJob{c: c, d: dx*dx + dz*dz})
|
||||
}
|
||||
sort.Slice(dirty, func(i, j int) bool { return dirty[i].d < dirty[j].d })
|
||||
for _, dj := range dirty {
|
||||
if time.Since(start) > 6*time.Millisecond {
|
||||
break // 帧预算(区块管理.md §4.3.2)
|
||||
}
|
||||
// 整个构建持一次读锁:邻居查询免逐次加锁(渲染.md §3.3)
|
||||
a.sess.World.WithReadLock(func() {
|
||||
m := a.meshBuilder.Build(dj.c, func(x, y, z int32) block.State {
|
||||
return a.sess.World.BlockUnlocked(x, y, z)
|
||||
})
|
||||
a.mu.Lock()
|
||||
a.pendingMesh = append(a.pendingMesh, &worldMeshJob{cx: dj.c.CX, cz: dj.c.CZ, m: m})
|
||||
a.mu.Unlock()
|
||||
})
|
||||
a.mu.Lock()
|
||||
a.pendingMesh = append(a.pendingMesh, &worldMeshJob{cx: c.CX, cz: c.CZ, m: m})
|
||||
a.mu.Unlock()
|
||||
c.Dirty.Store(false)
|
||||
dj.c.Dirty.Store(false)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -211,6 +242,35 @@ func (a *clientApp) Render() {
|
||||
}
|
||||
a.uiR.Flush()
|
||||
a.rend.EndFrame()
|
||||
|
||||
// 截图(dev 测试:游戏内等玩家所在区块 5×5 网格就绪再拍,避免视野空洞导致误判)
|
||||
a.frames++
|
||||
if *shotPath != "" && a.frames >= 200 {
|
||||
ready := a.screen != 1 // 非游戏画面(标题/暂停)无需等区块
|
||||
if !ready {
|
||||
ready = true
|
||||
pcx, pcz := int32(a.sess.Player.Pos[0])>>4, int32(a.sess.Player.Pos[2])>>4
|
||||
for cx := pcx - 2; cx <= pcx+2 && ready; cx++ {
|
||||
for cz := pcz - 2; cz <= pcz+2; cz++ {
|
||||
if !a.rend.HasChunk(cx, cz) {
|
||||
ready = false
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if ready || a.frames > 2400 { // 就绪即拍;48 秒兜底
|
||||
if !ready {
|
||||
a.logf("截图兜底:视野未完全就绪(frame=%d)", a.frames)
|
||||
}
|
||||
if err := a.saveScreenshot(*shotPath); err != nil {
|
||||
a.logf("截图失败: %v", err)
|
||||
} else {
|
||||
a.logf("截图已保存: %s", *shotPath)
|
||||
}
|
||||
a.rend.Window().SetShouldClose(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ShouldClose 窗口关闭判定。
|
||||
@@ -320,9 +380,24 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
// 4) 游戏会话(玩家先高空下落,区块加载后落地)
|
||||
p := physics.NewPlayer(8.5, 100, 8.5)
|
||||
cam := camera.New(p.Eye(), 0, 0, cfg.Client.FOV)
|
||||
// 4) 游戏会话(出生逻辑:先等出生区块加载,再定位地表出生——
|
||||
// 否则玩家从高空坠落会穿过未加载区块掉进地下,游戏内一片漆黑)
|
||||
var surf int32 = 120
|
||||
for deadline := time.Now().Add(15 * time.Second); time.Now().Before(deadline); {
|
||||
w.Update(8.5, 100, 8.5, 8*time.Millisecond)
|
||||
if w.Block(8, 80, 8) != block.Air {
|
||||
break
|
||||
}
|
||||
time.Sleep(2 * time.Millisecond)
|
||||
}
|
||||
for y := int32(255); y > 0; y-- {
|
||||
if w.Block(8, y, 8) != block.Air {
|
||||
surf = y
|
||||
break
|
||||
}
|
||||
}
|
||||
p := physics.NewPlayer(8.5, float64(surf+2), 8.5)
|
||||
cam := camera.New(p.Eye(), 0, -0.35, cfg.Client.FOV) // 略微俯视,视野内以地形为主
|
||||
inv := inventory.New(items)
|
||||
sess := game.NewSession(w, reg, items, recipes, p, inv, cam)
|
||||
|
||||
|
||||
33
cmd/client/devshot_gl.go
Normal file
33
cmd/client/devshot_gl.go
Normal file
@@ -0,0 +1,33 @@
|
||||
//go:build gl
|
||||
|
||||
// 开发测试工具:自动进游戏 + 截图(dev 测试通过后再打包)。
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image"
|
||||
"image/png"
|
||||
"os"
|
||||
|
||||
"github.com/go-gl/gl/v3.3-core/gl"
|
||||
)
|
||||
|
||||
// saveScreenshot 从默认帧缓冲读取像素并保存为 PNG(渲染线程调用)。
|
||||
func (a *clientApp) saveScreenshot(path string) error {
|
||||
w, h := int32(a.winW), int32(a.winH)
|
||||
pix := make([]uint8, w*h*4)
|
||||
gl.ReadPixels(0, 0, w, h, gl.RGBA, gl.UNSIGNED_BYTE, gl.Ptr(pix))
|
||||
img := image.NewRGBA(image.Rect(0, 0, int(w), int(h)))
|
||||
// 垂直翻转(GL 原点在左下)
|
||||
for y := int32(0); y < h; y++ {
|
||||
srcRow := (h - 1 - y) * w * 4
|
||||
dstRow := y * w * 4
|
||||
copy(img.Pix[dstRow:dstRow+w*4], pix[srcRow:srcRow+w*4])
|
||||
}
|
||||
f, err := os.Create(path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("创建截图文件: %w", err)
|
||||
}
|
||||
defer f.Close()
|
||||
return png.Encode(f, img)
|
||||
}
|
||||
Reference in New Issue
Block a user