Files
ngzz-mc/internal/ui/ui.go

323 lines
10 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, 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
}
// DrawTexImage 上传一张图像为纹理并绘制(长文本/图标用),返回纹理 ID 供缓存。
func (r *Renderer) DrawTexImage(x, y float32, img image.Image, scale float32) {
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, imageBytesUI(img))
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST)
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST)
r.pushQuad(quad{x: x, y: y, w: float32(b.Dx()) * scale, h: float32(b.Dy()) * scale,
u0: 0, v0: 0, u1: 1, v1: 1, r: 1, g: 1, b: 1, a: 1, tex: tex})
}
// UploadImage 上传图像为纹理并返回 ID(物品图标/长文本用)。
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, imageBytesUI(img))
gl.GenerateMipmap(gl.TEXTURE_2D)
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST_MIPMAP_LINEAR)
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 片元着色器(纹理 × 颜色,alpha 混合)。
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 = vec4(vColor.rgb, vColor.a) * vec4(1.0, 1.0, 1.0, t.a);
}
`
// 编译着色器(与 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 // 保持引用(长文本光栅化路径后置启用)