365 lines
11 KiB
Go
365 lines
11 KiB
Go
//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.1:Forward + 顶点光照)。
|
||
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);
|
||
}
|
||
`
|