Files
ngzz-mc/internal/render/renderer.go

241 lines
7.0 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
proj [16]float32
view [16]float32
}
// 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.False)
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)}
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)
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()
gl.Clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT)
}
// 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
}
// 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()
}
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;
out vec2 vUV;
out vec2 vLight;
void main() {
gl_Position = uViewProj * vec4(aPos, 1.0);
vUV = aUV;
vLight = aLight;
}
`
// chunkFragSrc 默认方块片元着色器cutout 剔除 + 光照亮度混合。
const chunkFragSrc = `
#version 330 core
in vec2 vUV;
in vec2 vLight;
uniform sampler2D uTex;
uniform float uCutout; // 1=alpha test树叶/玻璃0=普通/半透明
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, c.a);
}
`