feat(client): 手持物品渲染 + 动物方块模型 + 实体贴图 alpha test

- 手持非放置物品(工具/食物):实体图集物品图标小四边形(原版倾斜角,
  双面、alpha test),随挥动动画平移;可放置方块仍为六面立方体
- 动物(牛/猪/羊/鸡)改为方块组合模型(身体+头+腿),按原版皮肤布局
  取子矩形贴图,水平面向相机;修复动物 VAO 未捕获 EBO 导致的驱动崩溃
- 僵尸/村民/掉落物广告牌开启 alpha test(透明区域不再显黑块)
This commit is contained in:
NianGao Dev
2026-08-16 23:23:30 +08:00
parent 5d0524ca52
commit b0b632df05
7 changed files with 394 additions and 35 deletions

View File

@@ -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)