diff --git a/cmd/client/client_gl.go b/cmd/client/client_gl.go index 81fb6cbc..6e95eff9 100644 --- a/cmd/client/client_gl.go +++ b/cmd/client/client_gl.go @@ -118,14 +118,16 @@ type clientApp struct { cursorReq atomic.Int32 // 手持方块(tick 计算 UV → 渲染线程上传,渲染.md §5.2) - handInit bool - handSlot int - handHas bool - handDirty bool - handUVs [6][4]float32 - handTint [3]float32 // 手持颜色乘数(空手肤色拳 / 持方块 1,1,1) - swing float64 // 挥动动画剩余量(左/右键触发,渲染线程衰减) - swingTimer float64 // 按住挖掘时周期性挥动的节拍器 + handInit bool + handSlot int + handHas bool + handDirty bool + handUVs [6][4]float32 + handTint [3]float32 // 手持颜色乘数(空手肤色拳 / 持方块 1,1,1) + handItemHas bool // 手持非放置物品(图标四边形,实体图集 UV) + handItemUV [4]float32 + swing float64 // 挥动动画剩余量(左/右键触发,渲染线程衰减) + swingTimer float64 // 按住挖掘时周期性挥动的节拍器 // 水下状态(渲染线程读;tick 更新) underwater bool @@ -747,12 +749,30 @@ func (a *clientApp) computeHand() { return } d, ok := a.sess.Items.Get(sel.Name) - if !ok || d.Place == "" { - // 非可放置物品:同样显示肤色拳(原版按物品模型渲染,后置) + if !ok { a.computeFistHand() return } - bid, ok := a.reg.ID(d.Place) + if d.Place != "" { + a.computeBlockHand(d.Place) + return + } + // 非放置物品(工具/食物等):实体图集中含全部物品贴图,手持图标四边形 + if uv, found := a.rend.EntityUV(d.Texture); found { + a.mu.Lock() + a.handItemHas = true + a.handItemUV = uv + a.handHas = false + a.handDirty = true + a.mu.Unlock() + return + } + a.computeFistHand() +} + +// computeBlockHand 手持可放置方块:六面贴图立方体(渲染.md §5.2)。 +func (a *clientApp) computeBlockHand(place string) { + bid, ok := a.reg.ID(place) if !ok { a.computeFistHand() return @@ -770,6 +790,7 @@ func (a *clientApp) computeHand() { a.mu.Lock() a.handUVs = uvs a.handHas = true + a.handItemHas = false a.handTint = [3]float32{1, 1, 1} a.handDirty = true a.mu.Unlock() @@ -795,6 +816,7 @@ func (a *clientApp) computeFistHand() { a.mu.Lock() a.handUVs = [6][4]float32{uvs, uvs, uvs, uvs, uvs, uvs} a.handHas = true + a.handItemHas = false a.handTint = [3]float32{0.85, 0.65, 0.5} // 肤色 a.handDirty = true a.mu.Unlock() @@ -850,17 +872,21 @@ func (a *clientApp) Render() { chunks := a.sess.World.ActiveChunks() a.rend.DrawChunks(func(i int) (int32, int32) { return chunks[i].CX, chunks[i].CZ }, len(chunks)) - // 实体广告牌(渲染.md §5.3:僵尸/村民/动物/掉落物) + // 实体绘制(渲染.md §5.3:动物方块模型;僵尸/村民/掉落物广告牌) for _, e := range a.sess.Entities.List() { if e.Dead { continue } - key, h := entityVisual(e, a.sess.Items) + key, h, variant := entityVisual(e, a.sess.Items) if key == "" { continue } if uv, ok := a.rend.EntityUV(key); ok { - if e.Kind == entity.KindItemDrop { + if variant >= 0 { + // 动物:方块组合模型,始终面朝相机(原版观感) + yaw := float32(math.Atan2(a.sess.Cam.Pos[0]-e.Pos[0], a.sess.Cam.Pos[2]-e.Pos[2])) + a.rend.DrawAnimalModel(float32(e.Pos[0]), float32(e.Pos[1]), float32(e.Pos[2]), yaw, animalModel(variant), uv, e.HitFlash > 0) + } else if e.Kind == entity.KindItemDrop { a.rend.DrawBillboard(float32(e.Pos[0]), float32(e.Pos[1]), float32(e.Pos[2]), h, h, uv, false) } else { a.rend.DrawBillboard(float32(e.Pos[0]), float32(e.Pos[1]), float32(e.Pos[2]), h, h*0.6, uv, e.HitFlash > 0) @@ -888,6 +914,7 @@ func (a *clientApp) Render() { } a.mu.Lock() handDirty, handHas := a.handDirty, a.handHas + handItemHas, handItemUV := a.handItemHas, a.handItemUV a.handDirty = false var handUVs [6][4]float32 var handTint [3]float32 @@ -902,7 +929,7 @@ func (a *clientApp) Render() { swingPos[1] -= float32(a.swing) * 0.10 handDirty = true // 挥动期间逐帧重传位置 } - if handDirty { + if handDirty && !handItemHas { if handHas { a.rend.SetHandCube(handUVs, swingPos, 0.16) } else { @@ -910,7 +937,12 @@ func (a *clientApp) Render() { } } if a.screen == 1 { - a.rend.DrawHand(handTint) + if handItemHas { + // 手持非放置物品:物品图标四边形(随挥动位置移动) + a.rend.DrawHandItem(handItemUV, swingPos, 0.22) + } else { + a.rend.DrawHand(handTint) + } } // 准星指向方块名上传(tick 光栅化 → 渲染线程上传,UI还原.md §3.4) @@ -1196,6 +1228,17 @@ func main() { } defer rend.Shutdown() rend.SetAtlas(atlasImg) + // 窗口图标:素材包 pack.png(原版 logo,构建与发布.md §8) + for _, p := range am.Packs() { + iconPath := paths.Join(p.Root, "pack.png") + if f, err := os.Open(iconPath); err == nil { + if img, err := png.Decode(f); err == nil { + rend.SetWindowIcon(img) + } + f.Close() + break + } + } // 主菜单状态:鼠标可见(仅游戏内捕获,UI与输入.md §6) rend.SetCursorMode(false) // 裂纹图集上传(方块交互与动画.md §4) diff --git a/cmd/client/entities_gl.go b/cmd/client/entities_gl.go index df6aba65..d82f2505 100644 --- a/cmd/client/entities_gl.go +++ b/cmd/client/entities_gl.go @@ -1,43 +1,126 @@ //go:build gl -// 实体可视映射:实体类型 → 广告牌贴图键与高度(渲染.md §5.3)。 +// 实体可视映射:实体类型 → 贴图键/身高/动物模型(渲染.md §5.3)。 +// 动物用方块组合模型(身体+头+四腿)近似原版外观;僵尸/村民用交叉广告牌; +// 掉落物用物品图标小广告牌。 package main import ( "mc/internal/entity" "mc/internal/item" + "mc/internal/render" ) // entityVisual 返回实体的贴图键与身高(米)。 -// 动物按 ID 轮换四种外观(cow/pig/sheep/chicken);掉落物用物品图标(0.3 米小广告牌)。 -func entityVisual(e *entity.Entity, items *item.Registry) (key string, height float32) { +// 动物额外返回外观变体(0 牛 / 1 猪 / 2 羊 / 3 鸡),供方块模型选择。 +func entityVisual(e *entity.Entity, items *item.Registry) (key string, height float32, variant int) { switch e.Kind { case entity.KindZombie: - return "entity/zombie/zombie", 1.95 + return "entity/zombie/zombie", 1.95, -1 case entity.KindVillager: - return "entity/villager/villager", 1.95 + return "entity/villager/villager", 1.95, -1 case entity.KindAnimal: switch e.ID % 4 { case 0: - return "entity/cow/cow_temperate", 1.4 + return "entity/cow/cow_temperate", 1.4, 0 case 1: - return "entity/pig/pig_temperate", 0.9 + return "entity/pig/pig_temperate", 0.9, 1 case 2: - return "entity/sheep/sheep", 1.3 + return "entity/sheep/sheep", 1.3, 2 default: - return "entity/chicken/chicken_temperate", 0.7 + return "entity/chicken/chicken_temperate", 0.7, 3 } case entity.KindItemDrop: d, ok := e.Data.(*entity.Drop) if !ok || items == nil { - return "", 0 + return "", 0, -1 } def, ok := items.Get(d.Item) if !ok { - return "", 0 + return "", 0, -1 } - return def.Texture, 0.45 + return def.Texture, 0.45, -1 default: - return "", 0 + return "", 0, -1 + } +} + +// rect 构造图集 tile 内归一化子矩形。 +func rect(u0, v0, u1, v1 float32) [4]float32 { return [4]float32{u0, v0, u1, v1} } + +// boxPart 构造模型部件:六面统一用 side 矩形,前/后取中间条带,顶取上条带,底取下条带。 +func boxPart(cx, cy, cz, w, h, d float32, side [4]float32) render.AnimalPart { + u0, v0, u1, v1 := side[0], side[1], side[2], side[3] + uw, vh := u1-u0, v1-v0 + front := rect(u0+uw*0.35, v0, u0+uw*0.65, v1) // 前/后面:中间三分之一 + top := rect(u0, v0, u1, v0+vh*0.3) + bottom := rect(u0, v1-vh*0.3, u1, v1) + var uv [6][4]float32 + // 顺序同 cubeFaces:顶/底/东/西/南(前)/北(后) + uv[0], uv[1] = top, bottom + uv[2], uv[3] = side, side + uv[4], uv[5] = front, front + return render.AnimalPart{CX: cx, CY: cy, CZ: cz, W: w, H: h, D: d, UV: uv} +} + +// animalModel 返回动物方块模型(variant:0 牛 / 1 猪 / 2 羊 / 3 鸡)。 +// 贴图子矩形按原版皮肤布局:头部在纹理左侧、身体在右侧、腿在下部。 +// 图集 tile 内纹理被等比拉伸成 64×64(64×32 原图纵向拉长一倍,矩形已按拉伸后比例折算)。 +func animalModel(variant int) render.AnimalModel { + switch variant { + case 0: // 牛:纹理 64×64,内容在上半(v 0–0.5) + head := rect(0.02, 0.02, 0.48, 0.48) + body := rect(0.52, 0.02, 0.98, 0.48) + legs := rect(0.55, 0.30, 0.98, 0.50) + var m render.AnimalModel + m.Parts = append(m.Parts, + boxPart(0, 0.72, 0, 0.92, 0.62, 1.28, body), + boxPart(0, 1.26, 0.60, 0.56, 0.50, 0.56, head), + boxPart(-0.34, 0.24, 0.50, 0.22, 0.48, 0.22, legs), + boxPart(0.34, 0.24, 0.50, 0.22, 0.48, 0.22, legs), + boxPart(-0.34, 0.24, -0.50, 0.22, 0.48, 0.22, legs), + boxPart(0.34, 0.24, -0.50, 0.22, 0.48, 0.22, legs), + ) + return m + case 1: // 猪:纹理 64×64,内容在上半 + head := rect(0.14, 0.03, 0.48, 0.45) + body := rect(0.52, 0.03, 0.98, 0.45) + legs := rect(0.60, 0.30, 0.98, 0.50) + var m render.AnimalModel + m.Parts = append(m.Parts, + boxPart(0, 0.50, 0, 0.76, 0.44, 1.02, body), + boxPart(0, 0.86, 0.50, 0.50, 0.42, 0.50, head), + boxPart(-0.28, 0.15, 0.42, 0.18, 0.30, 0.18, legs), + boxPart(0.28, 0.15, 0.42, 0.18, 0.30, 0.18, legs), + boxPart(-0.28, 0.15, -0.42, 0.18, 0.30, 0.18, legs), + boxPart(0.28, 0.15, -0.42, 0.18, 0.30, 0.18, legs), + ) + return m + case 2: // 羊:纹理 64×32(tile 内 v 拉长一倍) + head := rect(0.02, 0.02, 0.26, 0.40) + body := rect(0.50, 0.15, 1.00, 0.72) + legs := rect(0.62, 0.50, 0.95, 0.85) + var m render.AnimalModel + m.Parts = append(m.Parts, + boxPart(0, 0.72, 0, 0.92, 0.60, 1.26, body), + boxPart(0, 1.20, 0.60, 0.50, 0.46, 0.50, head), + boxPart(-0.33, 0.21, 0.50, 0.20, 0.42, 0.20, legs), + boxPart(0.33, 0.21, 0.50, 0.20, 0.42, 0.20, legs), + boxPart(-0.33, 0.21, -0.50, 0.20, 0.42, 0.20, legs), + boxPart(0.33, 0.21, -0.50, 0.20, 0.42, 0.20, legs), + ) + return m + default: // 鸡:纹理 64×32 + head := rect(0.02, 0.05, 0.28, 0.55) + body := rect(0.28, 0.10, 0.70, 0.75) + legs := rect(0.30, 0.55, 0.60, 0.80) + var m render.AnimalModel + m.Parts = append(m.Parts, + boxPart(0, 0.36, 0, 0.44, 0.40, 0.52, body), + boxPart(0, 0.66, 0.24, 0.32, 0.30, 0.32, head), + boxPart(-0.14, 0.10, 0.18, 0.09, 0.20, 0.09, legs), + boxPart(0.14, 0.10, 0.18, 0.09, 0.20, 0.09, legs), + ) + return m } } diff --git a/internal/render/animals_gl.go b/internal/render/animals_gl.go new file mode 100644 index 00000000..7e21811c --- /dev/null +++ b/internal/render/animals_gl.go @@ -0,0 +1,125 @@ +//go:build gl + +// 动物方块模型渲染(渲染.md §5.3):身体/头/腿用方块组合近似原版动物外观, +// 贴图取自实体图集内该动物的纹理格子(子矩形映射到各面)。 +// 与区块共用着色器:alpha test 剔除透明、满亮不受光照影响。 +package render + +import ( + "math" + "unsafe" + + "mc/internal/mesh" + + "github.com/go-gl/gl/v3.3-core/gl" +) + +// AnimalPart 动物模型部件(相对脚底原点,模型前向 +z)。 +type AnimalPart struct { + CX, CY, CZ float32 // 部件中心 + W, H, D float32 // 部件尺寸 + // UV 六面在图集 tile 内归一化子矩形(u0,v0,u1,v1),顺序同 cubeFaces: + // 顶/底/东/西/南(前)/北(后) + UV [6][4]float32 +} + +// AnimalModel 动物模型:部件列表。 +type AnimalModel struct { + Parts []AnimalPart +} + +// DrawAnimalModel 在 (x,y,z)(脚底)以 yaw(0=朝 +z)绘制方块动物模型。 +// tile 为实体图集内动物纹理的归一化矩形;部件 UV 相对 tile 定义。 +// flash=true 受击白闪(uTint 提亮)。 +func (r *Renderer) DrawAnimalModel(x, y, z, yaw float32, model AnimalModel, tile [4]float32, flash bool) { + if r.entityTex == 0 || len(model.Parts) == 0 { + return + } + cosY := float32(math.Cos(float64(yaw))) + sinY := float32(math.Sin(float64(yaw))) + tw, th := tile[2]-tile[0], tile[3]-tile[1] + + verts := make([]mesh.Vertex, 0, len(model.Parts)*24) + for _, p := range model.Parts { + for fi := 0; fi < 6; fi++ { + uv := p.UV[fi] + u0 := tile[0] + uv[0]*tw + v0 := tile[1] + uv[1]*th + u1 := tile[0] + uv[2]*tw + v1 := tile[1] + uv[3]*th + for _, c := range cubeFaces[fi] { + lx := (c[0]-0.5)*p.W + p.CX + ly := (c[1]-0.5)*p.H + p.CY + lz := (c[2]-0.5)*p.D + p.CZ + // yaw 绕 Y 旋转:模型 +z → (sinY, cosY) + wx := lx*cosY + lz*sinY + wz := -lx*sinY + lz*cosY + var cu, cv float32 + switch fi { + case 0, 1: // 顶/底:x→u,z→v + cu, cv = c[0], c[2] + case 2, 3: // 东/西:z→u,y→v + cu, cv = c[2], 1-c[1] + default: // 南/北:x→u,y→v + cu, cv = c[0], 1-c[1] + } + verts = append(verts, mesh.Vertex{ + X: x + wx, Y: y + ly, Z: z + wz, + U: u0 + (u1-u0)*cu, V: v0 + (v1-v0)*cv, + Sky: 15, Block: 15, + }) + } + } + } + + if r.animalVao == 0 { + gl.GenVertexArrays(1, &r.animalVao) + gl.GenBuffers(1, &r.animalVbo) + gl.GenBuffers(1, &r.animalIbo) + gl.BindVertexArray(r.animalVao) + gl.BindBuffer(gl.ARRAY_BUFFER, r.animalVbo) + 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)) + // 索引缓冲绑定必须捕获进 VAO(ELEMENT_ARRAY_BUFFER 属 VAO 状态) + gl.BindBuffer(gl.ELEMENT_ARRAY_BUFFER, r.animalIbo) + gl.BindVertexArray(0) + } + + idx := make([]uint32, 0, len(verts)/4*6) + for base := 0; base+3 < len(verts); base += 4 { + b := uint32(base) + idx = append(idx, b, b+1, b+2, b+2, b+3, b) + } + // 数据上传须保持 VAO 绑定(避免污染其他 VAO 的 EBO 状态) + gl.BindVertexArray(r.animalVao) + gl.BindBuffer(gl.ARRAY_BUFFER, r.animalVbo) + gl.BufferData(gl.ARRAY_BUFFER, len(verts)*int(unsafe.Sizeof(mesh.Vertex{})), gl.Ptr(verts), gl.DYNAMIC_DRAW) + gl.BindBuffer(gl.ELEMENT_ARRAY_BUFFER, r.animalIbo) + gl.BufferData(gl.ELEMENT_ARRAY_BUFFER, len(idx)*4, gl.Ptr(idx), gl.DYNAMIC_DRAW) + + gl.UseProgram(r.prog) + gl.ActiveTexture(gl.TEXTURE0) + gl.BindTexture(gl.TEXTURE_2D, r.entityTex) + var vp [16]float32 + multiply4(&vp, &r.proj, &r.view) + gl.UniformMatrix4fv(gl.GetUniformLocation(r.prog, gl.Str("uViewProj\x00")), 1, false, &vp[0]) + gl.Uniform3f(gl.GetUniformLocation(r.prog, gl.Str("uOffset\x00")), 0, 0, 0) // 顶点已烘焙为世界坐标 + gl.Uniform1f(gl.GetUniformLocation(r.prog, gl.Str("uCutout\x00")), 1) // alpha test:剔除纹理透明区域 + if flash { + gl.Uniform4f(gl.GetUniformLocation(r.prog, gl.Str("uTint\x00")), 2.5, 2.5, 2.5, 1) + } else { + gl.Uniform4f(gl.GetUniformLocation(r.prog, gl.Str("uTint\x00")), 1, 1, 1, 1) + } + gl.Uniform2f(gl.GetUniformLocation(r.prog, gl.Str("uBill\x00")), 1, 0) // 非广告牌路径 + gl.Uniform2f(gl.GetUniformLocation(r.prog, gl.Str("uUVMin\x00")), 0, 0) + gl.Uniform2f(gl.GetUniformLocation(r.prog, gl.Str("uUVMax\x00")), 1, 1) + + gl.BindVertexArray(r.animalVao) + gl.DrawElements(gl.TRIANGLES, int32(len(idx)), gl.UNSIGNED_INT, gl.PtrOffset(0)) + gl.BindVertexArray(0) +} diff --git a/internal/render/entity_gl.go b/internal/render/entity_gl.go index 20f6d92b..791dd4fb 100644 --- a/internal/render/entity_gl.go +++ b/internal/render/entity_gl.go @@ -88,7 +88,7 @@ func (r *Renderer) DrawBillboard(x, y, z float32, h, w float32, uv [4]float32, f 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) + gl.Uniform1f(cutLoc, 1) // alpha test:剔除实体贴图透明区域(否则透明像素显黑块) tintLoc := gl.GetUniformLocation(r.prog, gl.Str("uTint\x00")) if flash { gl.Uniform4f(tintLoc, 2.5, 2.5, 2.5, 1) // 受击白闪 diff --git a/internal/render/overlay_gl.go b/internal/render/overlay_gl.go index 3f2c9162..ab2cc328 100644 --- a/internal/render/overlay_gl.go +++ b/internal/render/overlay_gl.go @@ -7,6 +7,7 @@ package render import ( "image" + "math" "unsafe" "mc/internal/mesh" @@ -234,3 +235,76 @@ func (r *Renderer) DrawHand(tint [3]float32) { gl.DrawElements(gl.TRIANGLES, 36, gl.UNSIGNED_INT, gl.PtrOffset(0)) gl.BindVertexArray(0) } + +// ensureHandItemQuad 惰性构建手持物品四边形 VAO(顶点每帧重传:位置随挥动变化)。 +func (r *Renderer) ensureHandItemQuad() { + if r.handItemVao != 0 { + return + } + gl.GenVertexArrays(1, &r.handItemVao) + gl.GenBuffers(1, &r.handItemVbo) + gl.GenBuffers(1, &r.handItemIbo) + gl.BindVertexArray(r.handItemVao) + gl.BindBuffer(gl.ARRAY_BUFFER, r.handItemVbo) + 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)) + idx := []uint32{0, 1, 2, 0, 2, 3} + gl.BindBuffer(gl.ELEMENT_ARRAY_BUFFER, r.handItemIbo) + gl.BufferData(gl.ELEMENT_ARRAY_BUFFER, len(idx)*4, gl.Ptr(idx), gl.STATIC_DRAW) + gl.BindVertexArray(0) +} + +// DrawHandItem 绘制手持物品图标(非放置物品,如工具/食物):视图空间小四边形, +// 轻微倾斜(原版手持角度),双面、alpha test,贴实体图集内物品贴图(渲染.md §5.2)。 +// pos 为视图空间中心,scale 为边长。 +func (r *Renderer) DrawHandItem(uv [4]float32, pos [3]float32, scale float32) { + if r.entityTex == 0 { + return + } + r.ensureHandItemQuad() + + // 四角局部坐标(-0.5..0.5),绕 Y 转 yaw、绕 X 抬 pitch(烘焙进视图空间顶点) + const yaw, pitch = -0.6, 0.25 + cy, sy := float32(math.Cos(yaw)), float32(math.Sin(yaw)) + cp, sp := float32(math.Cos(pitch)), float32(math.Sin(pitch)) + corners := [4][2]float32{{-0.5, -0.5}, {0.5, -0.5}, {0.5, 0.5}, {-0.5, 0.5}} + verts := make([]mesh.Vertex, 0, 4) + for _, c := range corners { + lx, ly := c[0]*scale, c[1]*scale + rx := lx * cy + rz := -lx * sy + ry := ly*cp - rz*sp + rz2 := ly*sp + rz*cp + verts = append(verts, mesh.Vertex{ + X: pos[0] + rx, Y: pos[1] + ry, Z: pos[2] + rz2, + U: c[0] + 0.5, V: 0.5 - c[1], // 图集 tile:v=0 为图像顶,图标保持正立 + Sky: 15, Block: 15, + }) + } + gl.BindBuffer(gl.ARRAY_BUFFER, r.handItemVbo) + gl.BufferData(gl.ARRAY_BUFFER, len(verts)*int(unsafe.Sizeof(mesh.Vertex{})), gl.Ptr(verts), gl.DYNAMIC_DRAW) + + gl.UseProgram(r.prog) + gl.ActiveTexture(gl.TEXTURE0) + gl.BindTexture(gl.TEXTURE_2D, r.entityTex) + gl.UniformMatrix4fv(gl.GetUniformLocation(r.prog, gl.Str("uViewProj\x00")), 1, false, &r.proj[0]) + gl.Uniform3f(gl.GetUniformLocation(r.prog, gl.Str("uOffset\x00")), 0, 0, 0) + gl.Uniform1f(gl.GetUniformLocation(r.prog, gl.Str("uCutout\x00")), 1) // alpha test 剔透明 + gl.Uniform4f(gl.GetUniformLocation(r.prog, gl.Str("uTint\x00")), 1, 1, 1, 1) + gl.Uniform2f(gl.GetUniformLocation(r.prog, gl.Str("uBill\x00")), 1, 0) + gl.Uniform2f(gl.GetUniformLocation(r.prog, gl.Str("uUVMin\x00")), uv[0], uv[1]) + gl.Uniform2f(gl.GetUniformLocation(r.prog, gl.Str("uUVMax\x00")), uv[2], uv[3]) + + gl.Disable(gl.CULL_FACE) // 双面(背面为图标反相) + gl.BindVertexArray(r.handItemVao) + gl.DrawElements(gl.TRIANGLES, 6, gl.UNSIGNED_INT, gl.PtrOffset(0)) + gl.BindVertexArray(0) + gl.Enable(gl.CULL_FACE) + gl.Uniform2f(gl.GetUniformLocation(r.prog, gl.Str("uUVMin\x00")), 0, 0) + gl.Uniform2f(gl.GetUniformLocation(r.prog, gl.Str("uUVMax\x00")), 1, 1) +} diff --git a/internal/render/renderer.go b/internal/render/renderer.go index 2b54fee0..f810ab2e 100644 --- a/internal/render/renderer.go +++ b/internal/render/renderer.go @@ -35,11 +35,17 @@ type Renderer struct { // 第一人称手持方块(渲染.md §5.2) handVao, handVbo, handIbo uint32 + // 手持物品图标(非放置物品:实体图集内物品贴图的小四边形) + handItemVao, handItemVbo, handItemIbo uint32 + // 实体广告牌(渲染.md §5.3) entityTex uint32 entityUV map[string][4]float32 billVao, billVbo, billIbo uint32 + // 动物方块模型(渲染.md §5.3:身体/头/腿方块组合,动态 VBO) + animalVao, animalVbo, animalIbo uint32 + proj [16]float32 view [16]float32 } @@ -174,6 +180,14 @@ func compileShader(kind uint32, src string) (uint32, error) { return s, nil } +// SetWindowIcon 设置窗口图标(素材包 pack.png 原版 logo,构建与发布.md §8)。 +func (r *Renderer) SetWindowIcon(img image.Image) { + if img == nil || r.window == nil { + return + } + r.window.SetIcon([]image.Image{img}) +} + // SetAtlas 上传纹理图集(最近邻采样 + mipmap,渲染.md §4)。 func (r *Renderer) SetAtlas(img image.Image) { if r.atlas == 0 { @@ -213,6 +227,22 @@ func (r *Renderer) Shutdown() { for _, c := range r.chunks { c.destroy() } + for _, v := range [][3]uint32{ + {r.handVao, r.handVbo, r.handIbo}, + {r.handItemVao, r.handItemVbo, r.handItemIbo}, + {r.billVao, r.billVbo, r.billIbo}, + {r.animalVao, r.animalVbo, r.animalIbo}, + } { + if v[0] != 0 { + gl.DeleteVertexArrays(1, &v[0]) + } + if v[1] != 0 { + gl.DeleteBuffers(1, &v[1]) + } + if v[2] != 0 { + gl.DeleteBuffers(1, &v[2]) + } + } if r.atlas != 0 { gl.DeleteTextures(1, &r.atlas) } diff --git a/文档/场景/渲染.md b/文档/场景/渲染.md index 03ae0587..a5402525 100644 --- a/文档/场景/渲染.md +++ b/文档/场景/渲染.md @@ -71,17 +71,21 @@ type Vertex struct { ### 5.2 覆盖层渲染(破坏裂纹与手持方块) - **破坏裂纹**(方块交互与动画.md §4):对破坏目标方块叠加一个**略外扩**(≈1.004)的单位立方体,六面贴 `block/destroy_stage_N.png`(N = 进度×10,0–9),满亮无光照;`gl.PolygonOffset` 防 z-fighting。裂纹图集独立打包上传(10 阶段)。 -- **第一人称手持方块**:视图空间固定位置/尺度的单位立方体(六面 UV 取自选中槽放置方块的 top/down/side),顶点直接以视图坐标构造,绘制时只乘投影矩阵。 +- **第一人称手持**: + - 空手:肤色拳(白羊毛纹理 × 肤色 tint); + - 手持**可放置方块**:视图空间固定位置/尺度的单位立方体(六面 UV 取自选中槽放置方块的 top/down/side); + - 手持**非放置物品**(工具/食物等):实体图集内物品贴图的**小四边形**,轻微倾斜、双面、alpha test(原版手持角度近似),随挥动动画平移。 + - 顶点直接以视图坐标构造,绘制时只乘投影矩阵。 - 二者与区块网格共用顶点格式与着色器,通过 `uOffset`(裂纹盒世界坐标平移)与「视图空间烘焙」(手持)区分。 - GL 铁律沿用 §2:裂纹/手持的 VBO 上传只发生在渲染线程(tick 只算 UV)。 -### 5.3 实体广告牌(MVP) +### 5.3 实体渲染(广告牌 + 动物方块模型) -- 实体(僵尸/村民/动物)以**相机朝向的四边形**渲染:脚底锚点 + 高×宽,贴 `entity/*.png`(图集打包,64px tile)。 +- **动物(牛/猪/羊/鸡)**:方块组合模型(身体 + 头 + 四腿,鸡为身体+头+两腿),贴图取自实体图集内动物皮肤的**子矩形**(头部在纹理左侧、身体在右侧、腿在下部,按原版皮肤布局折算);模型水平朝向相机(始终看到侧面)。顶点每帧动态上传(每只约 150 顶点,数量少无压力)。 +- **僵尸/村民/掉落物**:相机朝向的交叉四边形(脚底锚点 + 高×宽),贴 `entity/*.png`(图集打包,64px tile);**alpha test 开启**——否则纹理透明区域显黑块。 - 顶点着色器按 `uCamRight/uCamUp` 展开单位四边形(x∈[-0.5,0.5]、y∈[0,1],脚底为原点),`uUVMin/uUVMax` 把 UV 重映射到图集内对应实体格子;满亮不受光照影响。 -- 实体图集与方块图集分离(尺寸/采样不同);广告牌绘制时关闭背面剔除(始终面向相机)。 +- 实体图集与方块图集分离(尺寸/采样不同);广告牌绘制时关闭背面剔除(始终面向相机);动物模型开启背面剔除(方块正常绕序)。 - 动物按实体 ID 轮换 cow/pig/sheep/chicken 外观(物种系统后置时改为数据驱动)。 -- 掉落物渲染后置接入。 ## 6. 透明与半透明排序