344 lines
10 KiB
Go
344 lines
10 KiB
Go
//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
|
||
}
|
||
|
||
// Resize 更新窗口像素尺寸与整数 guiScale(渲染.md §5.4、UI还原.md §3.1)。
|
||
func (r *Renderer) Resize(winW, winH int, scale float32) {
|
||
if winW < 1 {
|
||
winW = 1
|
||
}
|
||
if winH < 1 {
|
||
winH = 1
|
||
}
|
||
r.w, r.h = int32(winW), int32(winH)
|
||
if scale < 1 {
|
||
scale = 1
|
||
}
|
||
r.scale = scale
|
||
}
|
||
|
||
// 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)
|
||
}
|
||
|
||
// DrawImage 按像素尺寸画一张已上传纹理。
|
||
func (r *Renderer) DrawImage(tex uint32, x, y, w, h float32) {
|
||
if r == nil || tex == 0 || w <= 0 || h <= 0 {
|
||
return
|
||
}
|
||
r.pushQuad(quad{x: x, y: y, w: w, h: h, u0: 0, v0: 0, u1: 1, v1: 1,
|
||
r: 1, g: 1, b: 1, a: 1, tex: tex})
|
||
}
|
||
|
||
// 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
|
||
}
|
||
|
||
// DeleteTexture 释放 UploadImage 产生的纹理(Loading 刷新日志行时复用)。
|
||
func (r *Renderer) DeleteTexture(id uint32) {
|
||
if r == nil || id == 0 {
|
||
return
|
||
}
|
||
gl.DeleteTextures(1, &id)
|
||
}
|
||
|
||
// 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 片元着色器:纹理色 × 颜色 tint(alpha 混合)。
|
||
// 注意:必须输出纹理 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 // 保持引用(长文本光栅化路径后置启用)
|