//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.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 } // 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, 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); } `