- 崩溃(index out of range):实体所在区块卸载后失去碰撞支撑无限下坠到 负坐标,光查询越界 panic——WorldView 增 ChunkLoaded,未加载区块实体冻结; chunk 光查询加越界防御(双保险) - 立面光照:mesh 每面取**邻格**光照(原版光照模型)——此前用方块自身光照 (sky=0)导致悬崖/矿洞立面全黑(泥土竖向黑色根因) - 掉落可见:生成后 1 秒拾取延迟 + 图标加大到 0.45 - 打击感:攻击命中音(block.hit)+ 实体受击白闪(uTint)+ 手持挥动动画 - 水下效果:相机入水蓝色叠加(渲染.md §8 水体) - 空手显示肤色拳头(白羊毛 × 肤色 tint),持方块正常显示 - HUD 布局:血条左下、饥饿右下(经典布局)
129 lines
5.2 KiB
Go
129 lines
5.2 KiB
Go
//go:build gl
|
||
|
||
// 实体广告牌渲染(实体系统.md §5、渲染.md §5.3):
|
||
// 以相机朝向的四边形绘制实体贴图(zombie/villager/动物)。
|
||
// 复用 chunk 着色器:aPos 为单位四边形局部坐标(x∈[-0.5,0.5],y∈[0,1],
|
||
// y=0 为脚底),顶点着色器按 uCamRight/uCamUp 展开成世界坐标;
|
||
// UV 经 uUVMin/uUVMax 重映射到图集内该实体的格子。
|
||
package render
|
||
|
||
import (
|
||
"image"
|
||
"math"
|
||
"unsafe"
|
||
|
||
"mc/internal/mesh"
|
||
|
||
"github.com/go-gl/gl/v3.3-core/gl"
|
||
)
|
||
|
||
// SetEntityAtlas 上传实体贴图图集并记录 UV(键 → 归一化 UV)。
|
||
func (r *Renderer) SetEntityAtlas(img image.Image, uv map[string][4]float32) {
|
||
if r.entityTex == 0 {
|
||
gl.GenTextures(1, &r.entityTex)
|
||
}
|
||
gl.BindTexture(gl.TEXTURE_2D, r.entityTex)
|
||
b := img.Bounds()
|
||
gl.TexImage2D(gl.TEXTURE_2D, 0, gl.RGBA8, int32(b.Dx()), int32(b.Dy()), 0, gl.RGBA, gl.UNSIGNED_BYTE, gl.Ptr(imageBytes(img)))
|
||
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST)
|
||
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)
|
||
r.entityUV = uv
|
||
}
|
||
|
||
// EntityUV 返回实体贴图 UV(不存在返回 false)。
|
||
func (r *Renderer) EntityUV(key string) ([4]float32, bool) {
|
||
uv, ok := r.entityUV[key]
|
||
return uv, ok
|
||
}
|
||
|
||
// ensureBillQuad 惰性构建单位广告牌四边形(pos+uv+light 顶点格式,CW 绕序正面朝相机)。
|
||
// aUV.y=0 对应纹理底部(脚底),1 对应顶部(头)——顶点序按此翻转 v。
|
||
func (r *Renderer) ensureBillQuad() {
|
||
if r.billVao != 0 {
|
||
return
|
||
}
|
||
verts := []mesh.Vertex{
|
||
{X: -0.5, Y: 0, Z: 0, U: 0, V: 1, Sky: 15, Block: 15}, // 脚底左(uv v=1)
|
||
{X: -0.5, Y: 1, Z: 0, U: 0, V: 0, Sky: 15, Block: 15}, // 头左(uv v=0)
|
||
{X: 0.5, Y: 1, Z: 0, U: 1, V: 0, Sky: 15, Block: 15}, // 头右
|
||
{X: 0.5, Y: 0, Z: 0, U: 1, V: 1, Sky: 15, Block: 15}, // 脚底右
|
||
}
|
||
idx := []uint32{0, 3, 1, 3, 2, 1} // 窗口坐标下 CW(FrontFace=CW 时为正面向相机)
|
||
|
||
gl.GenVertexArrays(1, &r.billVao)
|
||
gl.GenBuffers(1, &r.billVbo)
|
||
gl.GenBuffers(1, &r.billIbo)
|
||
gl.BindVertexArray(r.billVao)
|
||
gl.BindBuffer(gl.ARRAY_BUFFER, r.billVbo)
|
||
gl.BufferData(gl.ARRAY_BUFFER, len(verts)*int(unsafe.Sizeof(mesh.Vertex{})), gl.Ptr(verts), gl.STATIC_DRAW)
|
||
gl.BindBuffer(gl.ELEMENT_ARRAY_BUFFER, r.billIbo)
|
||
gl.BufferData(gl.ELEMENT_ARRAY_BUFFER, len(idx)*4, gl.Ptr(idx), gl.STATIC_DRAW)
|
||
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)
|
||
}
|
||
|
||
// DrawBillboard 在 (x,y,z)(脚底)绘制 h 高 × w 宽的实体广告牌,贴图 uv。
|
||
// flash=true 时整体白闪(受击反馈,打击感)。
|
||
func (r *Renderer) DrawBillboard(x, y, z float32, h, w float32, uv [4]float32, flash bool) {
|
||
if r.entityTex == 0 {
|
||
return
|
||
}
|
||
r.ensureBillQuad()
|
||
|
||
gl.UseProgram(r.prog)
|
||
gl.ActiveTexture(gl.TEXTURE0)
|
||
gl.BindTexture(gl.TEXTURE_2D, r.entityTex)
|
||
vpLoc := gl.GetUniformLocation(r.prog, gl.Str("uViewProj\x00"))
|
||
var vp [16]float32
|
||
multiply4(&vp, &r.proj, &r.view)
|
||
gl.UniformMatrix4fv(vpLoc, 1, false, &vp[0])
|
||
offLoc := gl.GetUniformLocation(r.prog, gl.Str("uOffset\x00"))
|
||
gl.Uniform3f(offLoc, x, y, z)
|
||
cutLoc := gl.GetUniformLocation(r.prog, gl.Str("uCutout\x00"))
|
||
gl.Uniform1f(cutLoc, 0)
|
||
tintLoc := gl.GetUniformLocation(r.prog, gl.Str("uTint\x00"))
|
||
if flash {
|
||
gl.Uniform4f(tintLoc, 2.5, 2.5, 2.5, 1) // 受击白闪
|
||
} else {
|
||
gl.Uniform4f(tintLoc, 1, 1, 1, 1)
|
||
}
|
||
// 相机基向量(视图矩阵列 0/1 即世界系下的 right/up)
|
||
rightLoc := gl.GetUniformLocation(r.prog, gl.Str("uCamRight\x00"))
|
||
upLoc := gl.GetUniformLocation(r.prog, gl.Str("uCamUp\x00"))
|
||
gl.Uniform3f(rightLoc, r.view[0], r.view[1], r.view[2])
|
||
gl.Uniform3f(upLoc, r.view[4], r.view[5], r.view[6])
|
||
billLoc := gl.GetUniformLocation(r.prog, gl.Str("uBill\x00"))
|
||
gl.Uniform2f(billLoc, w, h)
|
||
uvMinLoc := gl.GetUniformLocation(r.prog, gl.Str("uUVMin\x00"))
|
||
uvMaxLoc := gl.GetUniformLocation(r.prog, gl.Str("uUVMax\x00"))
|
||
gl.Uniform2f(uvMinLoc, uv[0], uv[1])
|
||
gl.Uniform2f(uvMaxLoc, uv[2], uv[3])
|
||
|
||
gl.Disable(gl.CULL_FACE) // 广告牌永远朝向相机,无需剔除
|
||
gl.BindVertexArray(r.billVao)
|
||
gl.DrawElements(gl.TRIANGLES, 6, gl.UNSIGNED_INT, gl.PtrOffset(0))
|
||
// 第二张交叉广告牌:水平轴换为视线方向的水平分量(俯视呈 + 形,
|
||
// 侧面不再穿帮成纸片——渲染.md §5.3)
|
||
fx, fz := -r.view[8], -r.view[10]
|
||
if fl := fx*fx + fz*fz; fl > 1e-6 {
|
||
inv := 1.0 / float32(math.Sqrt(float64(fl)))
|
||
gl.Uniform3f(rightLoc, fx*inv, 0, fz*inv)
|
||
gl.DrawElements(gl.TRIANGLES, 6, gl.UNSIGNED_INT, gl.PtrOffset(0))
|
||
}
|
||
gl.BindVertexArray(0)
|
||
gl.Enable(gl.CULL_FACE)
|
||
|
||
// 复位广告牌状态(避免影响后续裂纹/手持/UI)
|
||
gl.Uniform2f(billLoc, 1, 0) // uBill.y=0 → 顶点着色器走普通路径
|
||
gl.Uniform2f(uvMinLoc, 0, 0)
|
||
gl.Uniform2f(uvMaxLoc, 1, 1)
|
||
}
|