Files
ngzz-mc/internal/ui/ui.go
NianGao Dev fc151e7c07 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
2026-08-16 08:33:08 +08:00

312 lines
9.6 KiB
Go
Raw 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.
//go:build gl
// Package ui 2D UI 渲染器正交投影、四边形批处理、位图字体文本、九宫格UI还原.md §3
//
// 与 3D 渲染共用 GL 上下文但状态隔离UI与输入.md §2每帧 3D 之后调用,
// 内部保存/恢复必要状态。
package ui
import (
"fmt"
"image"
"strings"
"mc/internal/font"
"mc/internal/textr"
"github.com/go-gl/gl/v3.3-core/gl"
)
// quad 一个四边形屏幕像素坐标y 轴向下)。
type quad struct {
x, y, w, h float32
u0, v0, u1, v1 float32
r, g, b, a float32
tex uint32
}
// Renderer UI 渲染器。
type Renderer struct {
prog uint32
vao, vbo uint32
texWhite uint32
fontTex uint32
fontAtlas *font.Atlas
texCache map[string]uint32 // 长文本光栅化纹理缓存
quads []quad
w, h int32
scale float32 // 像素缩放guiScale
}
// New 创建 UI 渲染器(窗口逻辑分辨率 × 缩放)。
func New(winW, winH int, scale float32) (*Renderer, error) {
r := &Renderer{w: int32(winW), h: int32(winH), scale: scale, texCache: make(map[string]uint32)}
vs, err := compileShader(gl.VERTEX_SHADER, uiVertSrc)
if err != nil {
return nil, fmt.Errorf("ui.New 顶点着色器: %w", err)
}
fs, err := compileShader(gl.FRAGMENT_SHADER, uiFragSrc)
if err != nil {
return nil, fmt.Errorf("ui.New 片元着色器: %w", err)
}
prog := gl.CreateProgram()
gl.AttachShader(prog, vs)
gl.AttachShader(prog, fs)
gl.LinkProgram(prog)
var ok int32
gl.GetProgramiv(prog, gl.LINK_STATUS, &ok)
if ok == 0 {
return nil, fmt.Errorf("ui.New: 着色器链接失败")
}
gl.DeleteShader(vs)
gl.DeleteShader(fs)
r.prog = prog
gl.GenVertexArrays(1, &r.vao)
gl.GenBuffers(1, &r.vbo)
// 白色占位纹理
gl.GenTextures(1, &r.texWhite)
gl.BindTexture(gl.TEXTURE_2D, r.texWhite)
gl.TexImage2D(gl.TEXTURE_2D, 0, gl.RGBA8, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE, gl.Ptr([]uint8{255, 255, 255, 255}))
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST)
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST)
return r, nil
}
// SetFont 设置位图字体UI还原.md §4上传字形图集纹理
func (r *Renderer) SetFont(at *font.Atlas) {
r.fontAtlas = at
if at.Image == nil {
return
}
if r.fontTex == 0 {
gl.GenTextures(1, &r.fontTex)
}
gl.BindTexture(gl.TEXTURE_2D, r.fontTex)
b := at.Image.Bounds()
gl.TexImage2D(gl.TEXTURE_2D, 0, gl.RGBA8, int32(b.Dx()), int32(b.Dy()), 0, gl.RGBA, gl.UNSIGNED_BYTE, gl.Ptr(imageBytesUI(at.Image)))
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST)
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST)
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE)
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE)
}
// DrawRect 绘制纯色矩形。
func (r *Renderer) DrawRect(x, y, w, h float32, rgba [4]float32) {
r.pushQuad(quad{x: x, y: y, w: w, h: h, u0: 0, v0: 0, u1: 1, v1: 1,
r: rgba[0], g: rgba[1], b: rgba[2], a: rgba[3], tex: r.texWhite})
}
// DrawText 绘制文本ASCII 位图字形,含 1px 阴影UI还原.md §4
func (r *Renderer) DrawText(x, y float32, s string, scale float32, rgba [4]float32) {
if r.fontAtlas == nil {
return
}
s = strings.ReplaceAll(s, "§", "") // MVP忽略颜色码后置支持
cx := x
for _, ch := range s {
g, ok := r.fontAtlas.Glyphs[ch]
if !ok {
g = r.fontAtlas.Missing()
}
px := float32(g.X) / float32(r.fontAtlas.Image.Bounds().Dx())
py := float32(g.Y) / float32(r.fontAtlas.Image.Bounds().Dy())
pw := float32(g.W) / float32(r.fontAtlas.Image.Bounds().Dx())
ph := float32(g.H) / float32(r.fontAtlas.Image.Bounds().Dy())
gw := float32(g.W) * scale
gh := float32(g.H) * scale
// 阴影(黑色,偏移 1px
r.pushQuad(quad{x: cx + 1*scale, y: y + 1*scale, w: gw, h: gh, u0: px, v0: py, u1: px + pw, v1: py + ph,
r: 0.1, g: 0.1, b: 0.1, a: rgba[3] * 0.8, tex: r.fontTex})
r.pushQuad(quad{x: cx, y: y, w: gw, h: gh, u0: px, v0: py, u1: px + pw, v1: py + ph,
r: rgba[0], g: rgba[1], b: rgba[2], a: rgba[3], tex: r.fontTex})
cx += float32(g.Width) * scale
}
}
// TextWidth 文本像素宽度(布局用)。
func (r *Renderer) TextWidth(s string, scale float32) float32 {
if r.fontAtlas == nil {
return float32(len(s)) * 8 * scale
}
return float32(textrWidth(r.fontAtlas, s)) * scale
}
// textrWidth 计算文本宽度(复用 textr 逻辑的轻量版)。
func textrWidth(at *font.Atlas, s string) int {
w := 0
for _, ch := range s {
if g, ok := at.Glyphs[ch]; ok {
w += g.Width
} else {
w += 8
}
}
return w
}
// UploadImage 上传图像为纹理并返回 ID物品图标/长文本用)。
// 注意UI 纹理按 1:1 像素风绘制,不使用 mipmap——
// 非 2 次幂纹理 GenerateMipmap 在部分驱动上失败会导致纹理不完整、采样为纯白(白屏根因)。
func (r *Renderer) UploadImage(img image.Image) uint32 {
var tex uint32
gl.GenTextures(1, &tex)
gl.BindTexture(gl.TEXTURE_2D, tex)
b := img.Bounds()
gl.TexImage2D(gl.TEXTURE_2D, 0, gl.RGBA8, int32(b.Dx()), int32(b.Dy()), 0, gl.RGBA, gl.UNSIGNED_BYTE, gl.Ptr(imageBytesUI(img)))
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST)
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST)
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE)
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE)
return tex
}
// Begin 开始一帧 UI 绘制(清空批次)。
func (r *Renderer) Begin() { r.quads = r.quads[:0] }
// Flush 提交批次(切换投影 → 绘制 → 恢复状态)。
func (r *Renderer) Flush() {
if len(r.quads) == 0 {
return
}
// 保存 3D 状态
gl.Disable(gl.DEPTH_TEST)
gl.UseProgram(r.prog)
// 正交投影像素坐标y 向下)
var proj [16]float32
ortho2D(&proj, 0, float32(r.w), float32(r.h), 0)
projLoc := gl.GetUniformLocation(r.prog, gl.Str("uProj\x00"))
gl.UniformMatrix4fv(projLoc, 1, false, &proj[0])
sampLoc := gl.GetUniformLocation(r.prog, gl.Str("uTex\x00"))
gl.Uniform1i(sampLoc, 0)
// 批处理:合并为顶点数组(每 quad 6 顶点,含纹理 ID 通过多次绘制分组)
gl.BindVertexArray(r.vao)
gl.BindBuffer(gl.ARRAY_BUFFER, r.vbo)
gl.EnableVertexAttribArray(0) // pos
gl.EnableVertexAttribArray(1) // uv
gl.EnableVertexAttribArray(2) // color
stride := int32(9 * 4)
currentTex := uint32(0xFFFFFFFF)
flushBatch := func(tex uint32, verts []float32) {
if len(verts) == 0 {
return
}
gl.ActiveTexture(gl.TEXTURE0)
gl.BindTexture(gl.TEXTURE_2D, tex)
gl.BufferData(gl.ARRAY_BUFFER, len(verts)*4, gl.Ptr(verts), gl.STREAM_DRAW)
gl.VertexAttribPointer(0, 2, gl.FLOAT, false, stride, gl.PtrOffset(0))
gl.VertexAttribPointer(1, 2, gl.FLOAT, false, stride, gl.PtrOffset(2*4))
gl.VertexAttribPointer(2, 4, gl.FLOAT, false, stride, gl.PtrOffset(4*4))
gl.DrawArrays(gl.TRIANGLES, 0, int32(len(verts)/9))
}
var batch []float32
for _, q := range r.quads {
if q.tex != currentTex {
flushBatch(currentTex, batch)
batch = batch[:0]
currentTex = q.tex
}
batch = append(batch, quadVertices(q)...)
}
flushBatch(currentTex, batch)
gl.BindVertexArray(0)
gl.Enable(gl.DEPTH_TEST)
r.quads = r.quads[:0]
}
// pushQuad 入批。
func (r *Renderer) pushQuad(q quad) { r.quads = append(r.quads, q) }
// quadVertices 四边形 → 6 顶点2 三角形)。
func quadVertices(q quad) []float32 {
x0, y0 := q.x, q.y
x1, y1 := q.x+q.w, q.y+q.h
return []float32{
x0, y0, q.u0, q.v0, q.r, q.g, q.b, q.a, 0,
x1, y0, q.u1, q.v0, q.r, q.g, q.b, q.a, 0,
x1, y1, q.u1, q.v1, q.r, q.g, q.b, q.a, 0,
x0, y0, q.u0, q.v0, q.r, q.g, q.b, q.a, 0,
x1, y1, q.u1, q.v1, q.r, q.g, q.b, q.a, 0,
x0, y1, q.u0, q.v1, q.r, q.g, q.b, q.a, 0,
}
}
// ortho2D 正交投影矩阵(列主序,近 -1 远 1
func ortho2D(m *[16]float32, l, r, b, t float32) {
*m = [16]float32{}
m[0] = 2 / (r - l)
m[5] = 2 / (t - b)
m[10] = -1
m[12] = -(r + l) / (r - l)
m[13] = -(t + b) / (t - b)
m[15] = 1
}
// imageBytesUI image.Image → RGBA 字节流。
func imageBytesUI(img image.Image) []byte {
b := img.Bounds()
out := make([]byte, 0, b.Dx()*b.Dy()*4)
for y := b.Min.Y; y < b.Max.Y; y++ {
for x := b.Min.X; x < b.Max.X; x++ {
cr, cg, cb, ca := img.At(x, y).RGBA()
out = append(out, byte(cr>>8), byte(cg>>8), byte(cb>>8), byte(ca>>8))
}
}
return out
}
// uiVertSrc UI 顶点着色器。
const uiVertSrc = `
#version 330 core
layout(location = 0) in vec2 aPos;
layout(location = 1) in vec2 aUV;
layout(location = 2) in vec4 aColor;
uniform mat4 uProj;
out vec2 vUV;
out vec4 vColor;
void main() {
gl_Position = uProj * vec4(aPos, 0.0, 1.0);
vUV = aUV;
vColor = aColor;
}
`
// uiFragSrc UI 片元着色器:纹理色 × 颜色 tintalpha 混合)。
// 注意:必须输出纹理 RGB——否则彩色纹理按钮/背景)会被丢弃成纯白(白屏根因)。
const uiFragSrc = `
#version 330 core
in vec2 vUV;
in vec4 vColor;
uniform sampler2D uTex;
out vec4 fragColor;
void main() {
vec4 t = texture(uTex, vUV);
fragColor = t * vColor;
}
`
// compileShader 编译着色器(与 render 包一致的简化版)。
func compileShader(kind uint32, src string) (uint32, error) {
s := gl.CreateShader(kind)
cs, free := gl.Strs(src + "\x00")
gl.ShaderSource(s, 1, cs, nil)
free()
gl.CompileShader(s)
var ok int32
gl.GetShaderiv(s, gl.COMPILE_STATUS, &ok)
if ok == 0 {
var logLen int32
gl.GetShaderiv(s, gl.INFO_LOG_LENGTH, &logLen)
log := strings.Repeat(" ", int(logLen))
gl.GetShaderInfoLog(s, logLen, nil, gl.Str(log))
return 0, fmt.Errorf("着色器编译失败: %s", log)
}
return s, nil
}
var _ = textr.New // 保持引用(长文本光栅化路径后置启用)