feat(save): 存档系统——region 文件(zstd+CRC32+原子替换)、level.dat、区块序列化、世界存取(含测试)
This commit is contained in:
148
internal/render/mesh_gl.go
Normal file
148
internal/render/mesh_gl.go
Normal file
@@ -0,0 +1,148 @@
|
||||
package render
|
||||
|
||||
import (
|
||||
"unsafe"
|
||||
|
||||
"mc/internal/mesh"
|
||||
|
||||
"github.com/go-gl/gl/v3.3-core/gl"
|
||||
)
|
||||
|
||||
// chunkKey 区块坐标 → key(与 world 包一致)。
|
||||
func chunkKey(cx, cz int32) uint64 { return uint64(uint32(cx))<<32 | uint64(uint32(cz)) }
|
||||
|
||||
// UploadChunk 上传区块网格到 GPU(dirty 网格重建后调用,渲染.md §3.3)。
|
||||
// 三类网格合并为单 VBO + 单 IBO:不透明 → cutout → 半透明 顺序连续存放。
|
||||
func (r *Renderer) UploadChunk(cx, cz int32, m *mesh.ChunkMesh) {
|
||||
k := chunkKey(cx, cz)
|
||||
if old, ok := r.chunks[k]; ok {
|
||||
old.destroy()
|
||||
}
|
||||
cg := &chunkGL{}
|
||||
gl.GenVertexArrays(1, &cg.vao)
|
||||
gl.GenBuffers(1, &cg.vbo)
|
||||
gl.GenBuffers(1, &cg.ibo)
|
||||
gl.BindVertexArray(cg.vao)
|
||||
|
||||
// 合并顶点
|
||||
all := make([]mesh.Vertex, 0, len(m.Opaque)+len(m.Cutout)+len(m.Translucent))
|
||||
all = append(all, m.Opaque...)
|
||||
all = append(all, m.Cutout...)
|
||||
all = append(all, m.Translucent...)
|
||||
cg.translucent = len(m.Translucent) > 0
|
||||
|
||||
gl.BindBuffer(gl.ARRAY_BUFFER, cg.vbo)
|
||||
gl.BufferData(gl.ARRAY_BUFFER, len(all)*int(unsafe.Sizeof(mesh.Vertex{})), gl.Ptr(all), gl.STATIC_DRAW)
|
||||
|
||||
// 索引:每面 4 顶点四边形 → 2 三角形(0-1-2-2-3-0)
|
||||
idx := make([]uint32, 0, len(all)/4*6)
|
||||
for base := 0; base+3 < len(all); base += 4 {
|
||||
b := uint32(base)
|
||||
idx = append(idx, b, b+1, b+2, b+2, b+3, b)
|
||||
}
|
||||
gl.BindBuffer(gl.ELEMENT_ARRAY_BUFFER, cg.ibo)
|
||||
gl.BufferData(gl.ELEMENT_ARRAY_BUFFER, len(idx)*4, gl.Ptr(idx), gl.STATIC_DRAW)
|
||||
|
||||
// 顶点属性布局(渲染.md §5 顶点格式)
|
||||
stride := int32(unsafe.Sizeof(mesh.Vertex{}))
|
||||
gl.EnableVertexAttribArray(0)
|
||||
gl.VertexAttribPointer(0, 3, gl.FLOAT, false, stride, gl.PtrOffset(0))
|
||||
gl.EnableVertexAttribArray(1)
|
||||
gl.VertexAttribPointer(1, 2, gl.FLOAT, false, stride, gl.PtrOffset(3*4))
|
||||
gl.EnableVertexAttribArray(2)
|
||||
gl.VertexAttribPointer(2, 2, gl.UNSIGNED_BYTE, false, stride, gl.PtrOffset(5*4))
|
||||
|
||||
gl.BindVertexArray(0)
|
||||
cg.idxCount = int32(len(m.Opaque) / 4 * 6)
|
||||
cg.idxCut = int32(len(m.Cutout) / 4 * 6)
|
||||
cg.idxTrans = int32(len(m.Translucent) / 4 * 6)
|
||||
r.chunks[k] = cg
|
||||
}
|
||||
|
||||
// RemoveChunk 移除区块网格(区块卸载时调用)。
|
||||
func (r *Renderer) RemoveChunk(cx, cz int32) {
|
||||
k := chunkKey(cx, cz)
|
||||
if cg, ok := r.chunks[k]; ok {
|
||||
cg.destroy()
|
||||
delete(r.chunks, k)
|
||||
}
|
||||
}
|
||||
|
||||
// destroy 释放 GPU 资源。
|
||||
func (cg *chunkGL) destroy() {
|
||||
if cg.vao != 0 {
|
||||
gl.DeleteVertexArrays(1, &cg.vao)
|
||||
}
|
||||
if cg.vbo != 0 {
|
||||
gl.DeleteBuffers(1, &cg.vbo)
|
||||
}
|
||||
if cg.ibo != 0 {
|
||||
gl.DeleteBuffers(1, &cg.ibo)
|
||||
}
|
||||
cg.vao, cg.vbo, cg.ibo = 0, 0, 0
|
||||
}
|
||||
|
||||
// DrawChunks 绘制全部区块网格(三 pass,渲染.md §6)。
|
||||
// chunks:本帧要绘制的区块(已由视锥剔除过滤)。
|
||||
func (r *Renderer) DrawChunks(cx, cz func(i int) (int32, int32), n int) {
|
||||
gl.UseProgram(r.prog)
|
||||
gl.ActiveTexture(gl.TEXTURE0)
|
||||
gl.BindTexture(gl.TEXTURE_2D, r.atlas)
|
||||
|
||||
var vp [16]float32
|
||||
multiply4(&vp, &r.proj, &r.view)
|
||||
vpLoc := gl.GetUniformLocation(r.prog, gl.Str("uViewProj\x00"))
|
||||
cutLoc := gl.GetUniformLocation(r.prog, gl.Str("uCutout\x00"))
|
||||
gl.UniformMatrix4fv(vpLoc, 1, false, &vp[0])
|
||||
|
||||
// pass 1+2:不透明与 cutout(alpha test,不排序)
|
||||
gl.Uniform1f(cutLoc, 0)
|
||||
drawPass(r, n, cx, cz, false, false)
|
||||
gl.Uniform1f(cutLoc, 1)
|
||||
drawPass(r, n, cx, cz, false, true)
|
||||
// pass 3:半透明(关闭深度写入,远→近排序由调用方保证顺序)
|
||||
gl.Uniform1f(cutLoc, 0)
|
||||
gl.DepthMask(false)
|
||||
drawPass(r, n, cx, cz, true, false)
|
||||
gl.DepthMask(true)
|
||||
gl.BindVertexArray(0)
|
||||
}
|
||||
|
||||
// drawPass 按通道绘制。
|
||||
func drawPass(r *Renderer, n int, cx, cz func(i int) (int32, int32), translucent, cutout bool) {
|
||||
for i := 0; i < n; i++ {
|
||||
x, z := cx(i), cz(i)
|
||||
cg, ok := r.chunks[chunkKey(x, z)]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
base := int32(0)
|
||||
count := cg.idxCount
|
||||
if cutout {
|
||||
base = cg.idxCount
|
||||
count = cg.idxCut
|
||||
}
|
||||
if translucent {
|
||||
base = cg.idxCount + cg.idxCut
|
||||
count = cg.idxTrans
|
||||
}
|
||||
if count == 0 {
|
||||
continue
|
||||
}
|
||||
gl.BindVertexArray(cg.vao)
|
||||
gl.DrawElements(gl.TRIANGLES, count, gl.UNSIGNED_INT, gl.PtrOffset(int(base)*4))
|
||||
}
|
||||
}
|
||||
|
||||
// multiply4 列主序 4×4 矩阵乘法:out = a × b。
|
||||
func multiply4(out, a, b *[16]float32) {
|
||||
for col := 0; col < 4; col++ {
|
||||
for row := 0; row < 4; row++ {
|
||||
var v float32
|
||||
for k := 0; k < 4; k++ {
|
||||
v += a[k*4+row] * b[col*4+k]
|
||||
}
|
||||
out[col*4+row] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
238
internal/render/renderer.go
Normal file
238
internal/render/renderer.go
Normal file
@@ -0,0 +1,238 @@
|
||||
// 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);
|
||||
}
|
||||
`
|
||||
Reference in New Issue
Block a user