//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 // 保持引用(长文本光栅化路径后置启用)