Files
ngzz-mc/internal/render/mesh_gl.go
NianGao Dev d52bf1126f feat(client): 植被染色、破坏裂纹叠加、手持方块与实体广告牌渲染;原版 HUD 精灵
- 植被染色(渲染.md §4.1):blocks.json 增 tint 标记,图集构建后按
  colormap/grass|foliage 中心色乘色——原版材质包草顶/树叶为灰度图,不染色全灰
- 破坏裂纹(方块交互与动画.md §4):destroy_stage_0..9 裂纹盒叠加破坏目标方块
- 第一人称手持方块(渲染.md §5.2):选中槽放置方块六面 UV 视图空间立方体
- 实体广告牌(渲染.md §5.3):僵尸/村民/动物按贴图渲染,动物按 ID 轮换外观
- HUD 换原版精灵:crosshair/hotbar/hotbar_selection/heart/food(UI还原.md §3.4)
- 主菜单标题/按钮标签按请求字号光栅化(修复 24px 固定字号);版本角标 CJK
- 恢复背面剔除(相机修复后 '脚下黑块' 误诊解除);ESC 光标请求原子化防竞态
- dev 测试参数:-give/-crackstage/-spawntest/-esctest
2026-08-16 10:08:40 +08:00

165 lines
4.9 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
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)) }
// HasChunk 区块网格是否已上传(dev 截图测试等待视野就绪用)。
func (r *Renderer) HasChunk(cx, cz int32) bool {
_, ok := r.chunks[chunkKey(cx, cz)]
return ok
}
// 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(get 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])
offLoc := gl.GetUniformLocation(r.prog, gl.Str("uOffset\x00"))
gl.Uniform3f(offLoc, 0, 0, 0) // 区块网格无偏移(覆盖层在 DrawChunks 后自行设置)
billLoc := gl.GetUniformLocation(r.prog, gl.Str("uBill\x00"))
gl.Uniform2f(billLoc, 1, 0) // 非广告牌路径
uvMinLoc := gl.GetUniformLocation(r.prog, gl.Str("uUVMin\x00"))
uvMaxLoc := gl.GetUniformLocation(r.prog, gl.Str("uUVMax\x00"))
gl.Uniform2f(uvMinLoc, 0, 0) // 区块顶点 UV 直接使用
gl.Uniform2f(uvMaxLoc, 1, 1)
// pass 1+2:不透明与 cutout(alpha test,不排序)
gl.Uniform1f(cutLoc, 0)
drawPass(r, get, n, false, false)
gl.Uniform1f(cutLoc, 1)
drawPass(r, get, n, false, true)
// pass 3:半透明(关闭深度写入,远→近排序由调用方保证顺序)
gl.Uniform1f(cutLoc, 0)
gl.DepthMask(false)
drawPass(r, get, n, true, false)
gl.DepthMask(true)
gl.BindVertexArray(0)
}
// drawPass 按通道绘制。
func drawPass(r *Renderer, get func(i int) (int32, int32), n int, translucent, cutout bool) {
for i := 0; i < n; i++ {
x, z := get(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
}
}
}