Files
ngzz-mc/internal/render/renderer.go
2026-09-19 14:21:08 +08:00

365 lines
11 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 render 渲染层GLFW 窗口 + OpenGL 3.3 core 上下文 + 默认光影着色器(场景/渲染.md
//
// 线程铁律(渲染.md §2
// - 所有 GL 调用必须发生在同一 OS 线程runtime.LockOSThread
// - mesh 构建在 worker纯 CPU本包只负责上传与绘制
// - 渲染 goroutine 不分配大块内存(顶点缓冲复用)。
package render
import (
"fmt"
"image"
"runtime"
"strings"
"github.com/go-gl/gl/v3.3-core/gl"
"github.com/go-gl/glfw/v3.3/glfw"
)
// Renderer 渲染器:窗口 + 着色器 + 图集 + 区块网格缓存。
type Renderer struct {
window *glfw.Window
prog uint32 // 主着色程序chunk
atlas uint32 // 纹理图集
// 区块网格缓存key → VAO/VBO/索引数
chunks map[uint64]*chunkGL
// 裂纹覆盖层(方块交互与动画.md §4
crackTex uint32
crackUVs [10][4]float32
crackCubes [10]uint32
// 第一人称手持方块(渲染.md §5.2
handVao, handVbo, handIbo uint32
// 手持物品图标(非放置物品:实体图集内物品贴图的小四边形)
handItemVao, handItemVbo, handItemIbo uint32
// 实体广告牌(渲染.md §5.3
entityTex uint32
entityUV map[string][4]float32
billVao, billVbo, billIbo uint32
// 动物方块模型(渲染.md §5.3:身体/头/腿方块组合,动态 VBO
animalVao, animalVbo, animalIbo uint32
proj [16]float32
view [16]float32
fbW, fbH int // framebuffer 像素(渲染.md §5.4
fullscr bool
restX, restY, restW, restH int
}
// chunkGL 一个区块的 GPU 资源。
type chunkGL struct {
vao uint32
vbo uint32
ibo uint32
idxCount int32 // 不透明索引数
idxCut int32 // cutout 索引数(连续存放:不透明在前)
idxTrans int32 // translucent 索引数
translucent bool
}
// New 初始化 GLFW 与 GL创建窗口与着色器必须在主线程调用
func New(title string, width, height int) (*Renderer, error) {
runtime.LockOSThread() // GL 线程亲和(渲染.md §2 铁律)
if err := glfw.Init(); err != nil {
return nil, fmt.Errorf("render.New glfw.Init: %w", err)
}
glfw.WindowHint(glfw.ContextVersionMajor, 3)
glfw.WindowHint(glfw.ContextVersionMinor, 3)
glfw.WindowHint(glfw.OpenGLProfile, glfw.OpenGLCoreProfile)
glfw.WindowHint(glfw.Resizable, glfw.True)
glfw.WindowHint(glfw.Samples, 0)
win, err := glfw.CreateWindow(width, height, title, nil, nil)
if err != nil {
glfw.Terminate()
return nil, fmt.Errorf("render.New CreateWindow: %w", err)
}
win.MakeContextCurrent()
glfw.SwapInterval(1) // vsync
if err := gl.Init(); err != nil {
glfw.Terminate()
return nil, fmt.Errorf("render.New gl.Init: %w", err)
}
r := &Renderer{window: win, chunks: make(map[uint64]*chunkGL), restW: width, restH: height}
fw, fh := win.GetFramebufferSize()
if fw < 1 {
fw = width
}
if fh < 1 {
fh = height
}
r.fbW, r.fbH = fw, fh
gl.Viewport(0, 0, int32(fw), int32(fh))
win.SetFramebufferSizeCallback(func(_ *glfw.Window, w, h int) {
if w < 1 {
w = 1
}
if h < 1 {
h = 1
}
r.fbW, r.fbH = w, h
gl.Viewport(0, 0, int32(w), int32(h))
})
if err := r.initShaders(); err != nil {
glfw.Terminate()
return nil, err
}
// 初始图集1×1 白色占位SetAtlas 后替换)
img := image.NewRGBA(image.Rect(0, 0, 1, 1))
img.Set(0, 0, image.White)
r.SetAtlas(img)
gl.Enable(gl.DEPTH_TEST)
gl.Enable(gl.BLEND)
gl.BlendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA)
// 背面剔除:区块边界面向未加载邻居的内侧面从外部看是背面,不剔除会以暗色贴图
// 出现在视野里("脚下黑块"根因);同时省一半片元工作量。
// 本渲染器方块网格按 CW 绕序定义正面mesh.faceDef 角顺序)。
gl.Enable(gl.CULL_FACE)
gl.FrontFace(gl.CW)
gl.ClearColor(0.53, 0.81, 0.92, 1) // 天空蓝
return r, nil
}
// ShouldClose 窗口是否请求关闭。
func (r *Renderer) ShouldClose() bool { return r.window.ShouldClose() }
// BeginFrame 处理窗口事件并清屏(每帧开始,渲染.md §3.2 主循环)。
func (r *Renderer) BeginFrame() {
glfw.PollEvents()
w, h := r.window.GetFramebufferSize()
if w < 1 {
w = 1
}
if h < 1 {
h = 1
}
if w != r.fbW || h != r.fbH {
r.fbW, r.fbH = w, h
gl.Viewport(0, 0, int32(w), int32(h))
}
gl.Clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT)
}
// FramebufferSize 当前 framebuffer 像素尺寸(渲染.md §5.4)。
func (r *Renderer) FramebufferSize() (int, int) { return r.fbW, r.fbH }
// ToggleFullscreen F11 / Alt+Enter 切换独占全屏(渲染.md §5.4)。必须在 GL 线程调用。
func (r *Renderer) ToggleFullscreen() {
if r.fullscr {
r.window.SetMonitor(nil, r.restX, r.restY, r.restW, r.restH, 0)
r.fullscr = false
return
}
r.restX, r.restY = r.window.GetPos()
r.restW, r.restH = r.window.GetSize()
if r.restW < 1 {
r.restW = 1280
}
if r.restH < 1 {
r.restH = 720
}
mon := glfw.GetPrimaryMonitor()
if mon == nil {
return
}
mode := mon.GetVideoMode()
if mode == nil {
return
}
r.window.SetMonitor(mon, 0, 0, mode.Width, mode.Height, mode.RefreshRate)
r.fullscr = true
}
// EndFrame 交换缓冲(每帧结束)。
func (r *Renderer) EndFrame() { r.window.SwapBuffers() }
// Window 返回 GLFW 窗口(输入处理用)。
func (r *Renderer) Window() *glfw.Window { return r.window }
// SetCursorMode 鼠标捕获/释放UI与输入.md §6
func (r *Renderer) SetCursorMode(captured bool) {
if captured {
r.window.SetInputMode(glfw.CursorMode, glfw.CursorDisabled)
} else {
r.window.SetInputMode(glfw.CursorMode, glfw.CursorNormal)
}
}
// initShaders 编译默认光影着色器(渲染.md §9.1Forward + 顶点光照)。
func (r *Renderer) initShaders() error {
vs, err := compileShader(gl.VERTEX_SHADER, chunkVertSrc)
if err != nil {
return fmt.Errorf("render.initShaders 顶点着色器: %w", err)
}
fs, err := compileShader(gl.FRAGMENT_SHADER, chunkFragSrc)
if err != nil {
return fmt.Errorf("render.initShaders 片元着色器: %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 {
var logLen int32
gl.GetProgramiv(prog, gl.INFO_LOG_LENGTH, &logLen)
log := strings.Repeat(" ", int(logLen))
gl.GetProgramInfoLog(prog, logLen, nil, gl.Str(log))
return fmt.Errorf("着色器链接失败: %s", log)
}
gl.DeleteShader(vs)
gl.DeleteShader(fs)
r.prog = prog
return nil
}
// compileShader 编译单个着色器阶段。
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
}
// SetWindowIcon 设置窗口图标(素材包 pack.png 原版 logo构建与发布.md §8
func (r *Renderer) SetWindowIcon(img image.Image) {
if img == nil || r.window == nil {
return
}
r.window.SetIcon([]image.Image{img})
}
// SetAtlas 上传纹理图集(最近邻采样 + mipmap渲染.md §4
func (r *Renderer) SetAtlas(img image.Image) {
if r.atlas == 0 {
gl.GenTextures(1, &r.atlas)
}
gl.BindTexture(gl.TEXTURE_2D, r.atlas)
b := img.Bounds()
w, h := int32(b.Dx()), int32(b.Dy())
gl.TexImage2D(gl.TEXTURE_2D, 0, gl.RGBA8, w, h, 0, gl.RGBA, gl.UNSIGNED_BYTE, gl.Ptr(imageBytes(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)
}
// imageBytes 把 image.Image 转 RGBA 字节流。
func imageBytes(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++ {
r8, g8, b8, a8 := img.At(x, y).RGBA()
out = append(out, byte(r8>>8), byte(g8>>8), byte(b8>>8), byte(a8>>8))
}
}
return out
}
// SetViewProj 设置视图投影矩阵(每帧调用)。
func (r *Renderer) SetViewProj(view, proj [16]float32) {
r.view, r.proj = view, proj
}
// Shutdown 清理 GL 资源并关闭窗口。
func (r *Renderer) Shutdown() {
for _, c := range r.chunks {
c.destroy()
}
for _, v := range [][3]uint32{
{r.handVao, r.handVbo, r.handIbo},
{r.handItemVao, r.handItemVbo, r.handItemIbo},
{r.billVao, r.billVbo, r.billIbo},
{r.animalVao, r.animalVbo, r.animalIbo},
} {
if v[0] != 0 {
gl.DeleteVertexArrays(1, &v[0])
}
if v[1] != 0 {
gl.DeleteBuffers(1, &v[1])
}
if v[2] != 0 {
gl.DeleteBuffers(1, &v[2])
}
}
if r.atlas != 0 {
gl.DeleteTextures(1, &r.atlas)
}
if r.prog != 0 {
gl.DeleteProgram(r.prog)
}
r.window.Destroy()
glfw.Terminate()
}
// chunkVertSrc 默认方块顶点着色器GLSL 330渲染.md §9.1)。
const chunkVertSrc = `
#version 330 core
layout(location = 0) in vec3 aPos;
layout(location = 1) in vec2 aUV;
layout(location = 2) in vec2 aLight; // x=天空光 y=方块光4-bit
uniform mat4 uViewProj;
uniform vec3 uOffset; // 覆盖层/广告牌锚点平移(区块绘制时为 0
uniform vec3 uCamRight; // 相机右向量(世界系,广告牌展开用)
uniform vec3 uCamUp; // 相机上向量(世界系)
uniform vec2 uBill; // 广告牌缩放x=右 y=上y=0 时走普通路径
uniform vec2 uUVMin; // UV 重映射下限(区块/普通立方体为 0,0
uniform vec2 uUVMax; // UV 重映射上限(区块/普通立方体为 1,1
out vec2 vUV;
out vec2 vLight;
void main() {
vec3 pos = aPos + uOffset;
if (uBill.y > 0.001) {
pos = uOffset + vec3(aPos.x * uCamRight * uBill.x + aPos.y * uCamUp * uBill.y);
}
gl_Position = uViewProj * vec4(pos, 1.0);
vUV = uUVMin + aUV * (uUVMax - uUVMin);
vLight = aLight;
}
`
// chunkFragSrc 默认方块片元着色器cutout 剔除 + 光照亮度混合。
// uTint 颜色乘数(实体受击白闪/手持肤色,默认 1,1,1,1
const chunkFragSrc = `
#version 330 core
in vec2 vUV;
in vec2 vLight;
uniform sampler2D uTex;
uniform float uCutout; // 1=alpha test树叶/玻璃0=普通/半透明
uniform vec4 uTint;
out vec4 fragColor;
void main() {
vec4 c = texture(uTex, vUV);
if (uCutout > 0.5 && c.a < 0.5) discard;
float sky = vLight.x / 15.0;
float blk = vLight.y / 15.0;
float b = max(sky, blk * 0.9 + 0.06);
fragColor = vec4(c.rgb * b * uTint.rgb, c.a);
}
`