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
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -6,3 +6,6 @@ tools/mingw64/
|
||||
*.exe
|
||||
*.tmp
|
||||
*.log
|
||||
# 本地构建缓存(沙箱环境下 GOCACHE/zig cache 重定向到工作区)
|
||||
.gocache/
|
||||
.zigcache/
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"granite": { "id": 2, "hardness": 1.5, "resistance": 6.0, "light": 0, "transparent": false, "solid": true, "textures": { "all": "block/granite" } },
|
||||
"diorite": { "id": 3, "hardness": 1.5, "resistance": 6.0, "light": 0, "transparent": false, "solid": true, "textures": { "all": "block/diorite" } },
|
||||
"andesite": { "id": 4, "hardness": 1.5, "resistance": 6.0, "light": 0, "transparent": false, "solid": true, "textures": { "all": "block/andesite" } },
|
||||
"grass_block": { "id": 5, "hardness": 0.6, "resistance": 3.0, "light": 0, "transparent": false, "solid": true, "textures": { "up": "block/grass_block_top", "down": "block/dirt", "side": "block/grass_block_side" } },
|
||||
"grass_block": { "id": 5, "hardness": 0.6, "resistance": 3.0, "light": 0, "transparent": false, "solid": true, "textures": { "up": "block/grass_block_top", "down": "block/dirt", "side": "block/grass_block_side" }, "tint": "grass" },
|
||||
"dirt": { "id": 6, "hardness": 0.5, "resistance": 2.5, "light": 0, "transparent": false, "solid": true, "textures": { "all": "block/dirt" } },
|
||||
"cobblestone": { "id": 7, "hardness": 2.0, "resistance": 6.0, "light": 0, "transparent": false, "solid": true, "textures": { "all": "block/cobblestone" } },
|
||||
"oak_planks": { "id": 8, "hardness": 2.0, "resistance": 3.0, "light": 0, "transparent": false, "solid": true, "textures": { "all": "block/oak_planks" } },
|
||||
@@ -12,7 +12,7 @@
|
||||
"sand": { "id": 10, "hardness": 0.5, "resistance": 2.5, "light": 0, "transparent": false, "solid": true, "textures": { "all": "block/sand" } },
|
||||
"gravel": { "id": 11, "hardness": 0.6, "resistance": 3.0, "light": 0, "transparent": false, "solid": true, "textures": { "all": "block/gravel" } },
|
||||
"oak_log": { "id": 12, "hardness": 2.0, "resistance": 2.0, "light": 0, "transparent": false, "solid": true, "textures": { "up": "block/oak_log_top", "down": "block/oak_log_top", "side": "block/oak_log" } },
|
||||
"oak_leaves": { "id": 13, "hardness": 0.2, "resistance": 1.0, "light": 0, "transparent": true, "solid": true, "textures": { "all": "block/oak_leaves" }, "cutout": true },
|
||||
"oak_leaves": { "id": 13, "hardness": 0.2, "resistance": 1.0, "light": 0, "transparent": true, "solid": true, "textures": { "all": "block/oak_leaves" }, "cutout": true, "tint": "foliage" },
|
||||
"glass": { "id": 14, "hardness": 0.3, "resistance": 1.5, "light": 0, "transparent": true, "solid": true, "textures": { "all": "block/glass" }, "cutout": true },
|
||||
"water": { "id": 15, "hardness": 100.0, "resistance": 500.0, "light": 0, "transparent": true, "solid": false, "textures": { "all": "block/water_still" }, "translucent": true, "fluid": true },
|
||||
"lava": { "id": 16, "hardness": 100.0, "resistance": 500.0, "light": 15, "transparent": true, "solid": false, "textures": { "all": "block/lava_still" }, "translucent": true, "fluid": true },
|
||||
|
||||
@@ -9,10 +9,15 @@ package main
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"image"
|
||||
"image/png"
|
||||
"math"
|
||||
"os"
|
||||
"os/signal"
|
||||
"sort"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"mc/internal/assets"
|
||||
@@ -23,6 +28,7 @@ import (
|
||||
"mc/internal/config"
|
||||
"mc/internal/crash"
|
||||
"mc/internal/engine"
|
||||
"mc/internal/entity"
|
||||
"mc/internal/font"
|
||||
"mc/internal/game"
|
||||
"mc/internal/inventory"
|
||||
@@ -45,8 +51,14 @@ var devMode = flag.Bool("dev", devDefault, "开发模式:直接读取 assets/
|
||||
|
||||
// 开发测试参数(dev 测试通过后再打包)
|
||||
var (
|
||||
autoEnter = flag.Bool("auto", false, "自动进入游戏(跳过主菜单点击)")
|
||||
shotPath = flag.String("shot", "", "截图路径:运行约 2 秒后保存 PNG 并退出(dev 测试用)")
|
||||
autoEnter = flag.Bool("auto", false, "自动进入游戏(跳过主菜单点击)")
|
||||
shotPath = flag.String("shot", "", "截图路径:运行约 2 秒后保存 PNG 并退出(dev 测试用)")
|
||||
lookDeg = flag.Float64("lookdeg", 0, "dev 测试:进游戏后注入一次合成鼠标移动(角度),验证视角跟随链路")
|
||||
pitchDeg = flag.Float64("pitchdeg", 0, "dev 测试:进游戏后把俯仰角直接设置为该值(度)")
|
||||
giveItem = flag.String("give", "", "dev 测试:启动时给热键栏 0 号槽放入 64 个该物品(如 oak_planks/torch)")
|
||||
crackStage = flag.Int("crackstage", -1, "dev 测试:截图前把破坏进度视觉设置为该阶段(0–9)")
|
||||
spawnTest = flag.String("spawntest", "", "dev 测试:截图前在视线前方 5 格生成一个实体(zombie/animal/villager)")
|
||||
escTest = flag.Bool("esctest", false, "dev 测试:截图前注入一次 ESC(验证暂停菜单与光标释放链路)")
|
||||
)
|
||||
|
||||
// clientApp 客户端应用:把会话与渲染粘合起来(engine.Ticker + engine.Renderer)。
|
||||
@@ -72,14 +84,32 @@ type clientApp struct {
|
||||
remotePlayers map[uint32][3]float64
|
||||
|
||||
meshBuilder *mesh.Builder
|
||||
uv func(string) (u0, v0, u1, v1 float32, ok bool)
|
||||
|
||||
mu sync.Mutex
|
||||
pendingMesh []*worldMeshJob // tick 构建 → 渲染线程上传
|
||||
|
||||
// 光标模式请求(-1 无请求 / 0 释放 / 1 捕获):由 tick 写入、渲染线程消费。
|
||||
// GLFW 调用必须发生在主线程(渲染线程),从 tick goroutine 直接调用
|
||||
// SetInputMode 属线程违规——症状:ESC 后鼠标仍锁在窗口中心无法自由移动。
|
||||
cursorReq atomic.Int32
|
||||
|
||||
// 手持方块(tick 计算 UV → 渲染线程上传,渲染.md §5.2)
|
||||
handInit bool
|
||||
handSlot int
|
||||
handHas bool
|
||||
handDirty bool
|
||||
handUVs [6][4]float32
|
||||
|
||||
winW, winH int
|
||||
tickCnt uint64
|
||||
frames int // 渲染帧计数(截图时机用)
|
||||
lastTime time.Time
|
||||
|
||||
lookInjected bool // dev 测试:-lookdeg 已注入
|
||||
crackShot bool // dev 测试:-crackstage 已注入
|
||||
spawnShot bool // dev 测试:-spawntest 已注入
|
||||
escShot bool // dev 测试:-esctest 已注入
|
||||
}
|
||||
|
||||
// worldMeshJob 待上传网格。
|
||||
@@ -96,21 +126,21 @@ func (a *clientApp) Tick(dt float64) {
|
||||
// dev 测试:-auto 自动进入游戏(跳过点击)
|
||||
if *autoEnter {
|
||||
a.screen = 1
|
||||
a.rend.SetCursorMode(true)
|
||||
a.requestCursor(true)
|
||||
return
|
||||
}
|
||||
if a.in.clicked {
|
||||
switch a.titleScreen.HitTest(a.in.clickX, a.in.clickY) {
|
||||
case "play":
|
||||
a.screen = 1
|
||||
a.rend.SetCursorMode(true)
|
||||
a.requestCursor(true)
|
||||
case "multi":
|
||||
// 联机:连接本机服务器(多人同步.md §2)
|
||||
if err := dialServer(a, "127.0.0.1:25565", "玩家"); err != nil {
|
||||
a.logf("连接服务器失败: %v", err)
|
||||
} else {
|
||||
a.screen = 1
|
||||
a.rend.SetCursorMode(true)
|
||||
a.requestCursor(true)
|
||||
a.hud.OnlineCount = func() int {
|
||||
a.remoteMu.Lock()
|
||||
defer a.remoteMu.Unlock()
|
||||
@@ -128,13 +158,13 @@ func (a *clientApp) Tick(dt float64) {
|
||||
case 2: // 暂停
|
||||
if a.in.escPressed {
|
||||
a.screen = 1
|
||||
a.rend.SetCursorMode(true)
|
||||
a.requestCursor(true)
|
||||
}
|
||||
if a.in.clicked {
|
||||
switch a.pauseScreen.HitTest(a.in.clickX, a.in.clickY) {
|
||||
case "resume":
|
||||
a.screen = 1
|
||||
a.rend.SetCursorMode(true)
|
||||
a.requestCursor(true)
|
||||
case "quit":
|
||||
a.rend.Window().SetShouldClose(true)
|
||||
}
|
||||
@@ -146,11 +176,28 @@ func (a *clientApp) Tick(dt float64) {
|
||||
// 游戏中
|
||||
if a.in.escPressed {
|
||||
a.screen = 2
|
||||
a.rend.SetCursorMode(false)
|
||||
a.requestCursor(false)
|
||||
a.in.escPressed = false
|
||||
return
|
||||
}
|
||||
a.in.apply(a.sess)
|
||||
// 手持方块 UV(选中槽变化时重算;渲染线程消费,渲染.md §5.2)
|
||||
if !a.handInit || a.sess.Inv.Selected != a.handSlot {
|
||||
a.computeHand()
|
||||
}
|
||||
// dev 测试:注入一次合成鼠标移动(走真实 cursorCallback,验证视角跟随链路)
|
||||
if (*lookDeg != 0 || *pitchDeg != 0) && !a.lookInjected {
|
||||
a.lookInjected = true
|
||||
if *pitchDeg != 0 {
|
||||
a.sess.Cam.Pitch = *pitchDeg * math.Pi / 180.0
|
||||
}
|
||||
if *lookDeg != 0 {
|
||||
dx := float64(*lookDeg) * math.Pi / 180.0 / 0.0025 // 目标弧度 → 像素位移
|
||||
a.in.cursorCallback(nil, float64(a.winW)/2, float64(a.winH)/2) // 复位基准
|
||||
a.in.cursorCallback(nil, float64(a.winW)/2+dx, float64(a.winH)/2)
|
||||
a.logf("lookdeg 注入完成: +%.1f° 水平移动", *lookDeg)
|
||||
}
|
||||
}
|
||||
a.sess.Tick(dt)
|
||||
|
||||
// 联机:上报移动(20Hz,多人同步.md §3)
|
||||
@@ -207,8 +254,111 @@ func (a *clientApp) Tick(dt float64) {
|
||||
}
|
||||
}
|
||||
|
||||
// injectDevVisuals dev 测试注入(渲染线程,3D 绘制之前调用):
|
||||
// -crackstage 设置破坏裂纹阶段(视线前方方块);-spawntest 生成实体;
|
||||
// -esctest 注入 ESC(下一 tick 转暂停并请求释放光标)。
|
||||
func (a *clientApp) injectDevVisuals() {
|
||||
if *escTest && !a.escShot {
|
||||
a.escShot = true
|
||||
a.in.escPressed = true
|
||||
a.logf("esctest 注入: ESC 按下(暂停菜单 + 光标释放链路)")
|
||||
}
|
||||
if *crackStage >= 0 && !a.crackShot {
|
||||
a.crackShot = true
|
||||
if h, ok := a.sess.Cast(4); ok {
|
||||
a.sess.SetBreakVisual(h.X, h.Y, h.Z, float64(*crackStage)/10.0+0.05)
|
||||
a.in.breakHeld = true // 防止 tick 的 EndBreak 清掉注入的裂纹视觉
|
||||
a.logf("crackstage 注入: 方块(%d,%d,%d) 阶段 %d", h.X, h.Y, h.Z, *crackStage)
|
||||
}
|
||||
}
|
||||
if *spawnTest != "" && !a.spawnShot {
|
||||
a.spawnShot = true
|
||||
f := a.sess.Cam.Forward()
|
||||
ex, ez := int32(a.sess.Player.Pos[0]+f[0]*5), int32(a.sess.Player.Pos[2]+f[2]*5)
|
||||
gy := int32(a.sess.Player.Pos[1])
|
||||
for ; gy > 0; gy-- {
|
||||
if a.sess.World.Block(ex, gy, ez) != block.Air {
|
||||
break
|
||||
}
|
||||
}
|
||||
ey := float64(gy + 1)
|
||||
switch *spawnTest {
|
||||
case "zombie":
|
||||
a.sess.Entities.Spawn(entity.KindZombie, float64(ex)+0.5, ey, float64(ez)+0.5)
|
||||
case "animal":
|
||||
e := a.sess.Entities.Spawn(entity.KindAnimal, float64(ex)+0.5, ey, float64(ez)+0.5)
|
||||
e.Data = &entity.Animal{}
|
||||
case "villager":
|
||||
a.sess.Entities.Spawn(entity.KindVillager, float64(ex)+0.5, ey, float64(ez)+0.5)
|
||||
}
|
||||
a.logf("spawntest 注入: %s 于 (%d,%d,%d)", *spawnTest, ex, gy+1, ez)
|
||||
}
|
||||
}
|
||||
|
||||
// requestCursor 请求切换光标捕获模式(由 tick 写入,渲染线程消费——
|
||||
// GLFW 调用必须发生在渲染/主线程,UI与输入.md §6)。
|
||||
func (a *clientApp) requestCursor(captured bool) {
|
||||
if captured {
|
||||
a.cursorReq.Store(1)
|
||||
} else {
|
||||
a.cursorReq.Store(0)
|
||||
}
|
||||
}
|
||||
|
||||
// computeHand 计算手持方块的六面 UV(选中槽的放置方块)。
|
||||
// 顺序:顶/底/东/西/南/北(与 render.cubeFaces 一致)。
|
||||
func (a *clientApp) computeHand() {
|
||||
a.handInit = true
|
||||
a.handSlot = a.sess.Inv.Selected
|
||||
setEmpty := func() {
|
||||
a.mu.Lock()
|
||||
a.handHas = false
|
||||
a.handDirty = true
|
||||
a.mu.Unlock()
|
||||
}
|
||||
sel := a.sess.Inv.SelectedStack()
|
||||
if sel.Empty() {
|
||||
setEmpty()
|
||||
return
|
||||
}
|
||||
d, ok := a.sess.Items.Get(sel.Name)
|
||||
if !ok || d.Place == "" {
|
||||
setEmpty()
|
||||
return
|
||||
}
|
||||
bid, ok := a.reg.ID(d.Place)
|
||||
if !ok {
|
||||
setEmpty()
|
||||
return
|
||||
}
|
||||
st := block.NewState(bid, 0)
|
||||
faces := [6]string{"up", "down", "side", "side", "side", "side"}
|
||||
var uvs [6][4]float32
|
||||
for i, f := range faces {
|
||||
u0, v0, u1, v1, found := a.uv(a.reg.Texture(st, f))
|
||||
if !found {
|
||||
u0, v0, u1, v1 = 0, 0, 1, 1
|
||||
}
|
||||
uvs[i] = [4]float32{u0, v0, u1, v1}
|
||||
}
|
||||
a.mu.Lock()
|
||||
a.handUVs = uvs
|
||||
a.handHas = true
|
||||
a.handDirty = true
|
||||
a.mu.Unlock()
|
||||
}
|
||||
|
||||
// Render 每帧渲染(渲染线程):上传待传网格 → 视图矩阵 → 三 pass 绘制。
|
||||
func (a *clientApp) Render() {
|
||||
// 消费光标模式请求(主线程执行 GLFW 调用)
|
||||
if req := a.cursorReq.Load(); req >= 0 {
|
||||
a.rend.SetCursorMode(req == 1)
|
||||
a.cursorReq.Store(-1)
|
||||
}
|
||||
// dev 测试:截图前注入破坏裂纹/实体/ESC(必须在 3D 绘制之前,截图帧才能拍到)
|
||||
if a.frames == 195 && (*crackStage >= 0 || *spawnTest != "" || *escTest) {
|
||||
a.injectDevVisuals()
|
||||
}
|
||||
// 上传 tick 构建好的网格(GL 调用必须在渲染线程)
|
||||
a.mu.Lock()
|
||||
pending := a.pendingMesh
|
||||
@@ -228,6 +378,44 @@ func (a *clientApp) Render() {
|
||||
a.rend.BeginFrame()
|
||||
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:僵尸/村民/动物)
|
||||
for _, e := range a.sess.Entities.List() {
|
||||
if e.Dead {
|
||||
continue
|
||||
}
|
||||
key, h := entityVisual(e)
|
||||
if key == "" {
|
||||
continue
|
||||
}
|
||||
if uv, ok := a.rend.EntityUV(key); ok {
|
||||
a.rend.DrawBillboard(float32(e.Pos[0]), float32(e.Pos[1]), float32(e.Pos[2]), h, h*0.6, uv)
|
||||
}
|
||||
}
|
||||
|
||||
// 破坏裂纹叠加(方块交互与动画.md §4:progress→destroy_stage 0–9)
|
||||
if prog := a.sess.BreakProgress(); prog > 0 {
|
||||
t := a.sess.BreakTarget()
|
||||
a.rend.DrawCrackOverlay(float32(t.X), float32(t.Y), float32(t.Z), int(prog*10))
|
||||
}
|
||||
// 手持方块上传与绘制(渲染.md §5.2;上传必须是渲染线程)
|
||||
a.mu.Lock()
|
||||
handDirty, handHas := a.handDirty, a.handHas
|
||||
a.handDirty = false
|
||||
var handUVs [6][4]float32
|
||||
if handHas {
|
||||
handUVs = a.handUVs
|
||||
}
|
||||
a.mu.Unlock()
|
||||
if handDirty {
|
||||
if handHas {
|
||||
a.rend.SetHandCube(handUVs, [3]float32{0.46, -0.44, -0.66}, 0.16)
|
||||
} else {
|
||||
a.rend.ClearHand()
|
||||
}
|
||||
}
|
||||
a.rend.DrawHand()
|
||||
|
||||
// UI pass(UI与输入.md §2:3D 之后、状态隔离)
|
||||
a.uiR.Begin()
|
||||
mx, my := float32(a.in.cursorX), float32(a.in.cursorY)
|
||||
@@ -263,6 +451,7 @@ func (a *clientApp) Render() {
|
||||
if !ready {
|
||||
a.logf("截图兜底:视野未完全就绪(frame=%d)", a.frames)
|
||||
}
|
||||
a.logf("shot cam: pos=%v yaw=%.2f pitch=%.2f chunks=%d", a.sess.Cam.Pos, a.sess.Cam.Yaw, a.sess.Cam.Pitch, len(a.sess.World.ActiveChunks()))
|
||||
if err := a.saveScreenshot(*shotPath); err != nil {
|
||||
a.logf("截图失败: %v", err)
|
||||
} else {
|
||||
@@ -360,6 +549,37 @@ func main() {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// 植被染色(渲染.md §4.1):原版材质包草顶/树叶为灰度图,
|
||||
// 绿色来自染色表——图集构建后对带 tint 标记的纹理乘色,否则草地/树叶全灰。
|
||||
if tinted := reg.TintTextureKeys(); len(tinted) > 0 {
|
||||
atlasImg = applyAtlasTint(atlasImg, uvRects, am, tinted)
|
||||
log.Infof("图集植被染色:%v", tinted)
|
||||
}
|
||||
|
||||
// 裂纹图集(方块交互与动画.md §4:destroy_stage_0..9,破坏进度叠加用)
|
||||
crackKeys := make([]string, 10)
|
||||
for i := range crackKeys {
|
||||
crackKeys[i] = fmt.Sprintf("block/destroy_stage_%d", i)
|
||||
}
|
||||
crackImg, crackRectMap, err := atlas.Build(am.ResolveTexture, crackKeys, 16)
|
||||
if err != nil {
|
||||
log.Warnf("裂纹图集构建失败:%v", err)
|
||||
}
|
||||
|
||||
// 实体贴图图集(渲染.md §5.3:僵尸/村民/动物的广告牌贴图)
|
||||
entityKeys := []string{
|
||||
"entity/zombie/zombie",
|
||||
"entity/villager/villager",
|
||||
"entity/cow/cow_temperate",
|
||||
"entity/pig/pig_temperate",
|
||||
"entity/sheep/sheep",
|
||||
"entity/chicken/chicken_temperate",
|
||||
}
|
||||
entityImg, entityRectMap, err := atlas.Build(am.ResolveTexture, entityKeys, 64)
|
||||
if err != nil {
|
||||
log.Warnf("实体图集构建失败:%v", err)
|
||||
}
|
||||
|
||||
// 3) 世界与生成器(区块管理.md §4 异步流水线)
|
||||
gen, err := worldgen.New(reg, 123456789, 0.005, 0.01, 0.1)
|
||||
if err != nil {
|
||||
@@ -399,6 +619,14 @@ func main() {
|
||||
p := physics.NewPlayer(8.5, float64(surf+2), 8.5)
|
||||
cam := camera.New(p.Eye(), 0, -0.35, cfg.Client.FOV) // 略微俯视,视野内以地形为主
|
||||
inv := inventory.New(items)
|
||||
// dev 测试:给 0 号槽物品(验证手持方块渲染)
|
||||
if *giveItem != "" {
|
||||
if inv.Add(item.Stack{Name: *giveItem, Count: 64}).Count != 0 {
|
||||
log.Warnf("give 失败(物品不存在或已满): %s", *giveItem)
|
||||
} else {
|
||||
log.Infof("dev give: %s ×64 → 热键栏 0 号槽", *giveItem)
|
||||
}
|
||||
}
|
||||
sess := game.NewSession(w, reg, items, recipes, p, inv, cam)
|
||||
|
||||
// 5) 渲染器(GL 线程亲和在其内部锁定)
|
||||
@@ -410,6 +638,27 @@ func main() {
|
||||
rend.SetAtlas(atlasImg)
|
||||
// 主菜单状态:鼠标可见(仅游戏内捕获,UI与输入.md §6)
|
||||
rend.SetCursorMode(false)
|
||||
// 裂纹图集上传(方块交互与动画.md §4)
|
||||
if crackImg != nil {
|
||||
var crackRects [10]image.Rectangle
|
||||
for i := 0; i < 10; i++ {
|
||||
crackRects[i] = crackRectMap[crackKeys[i]]
|
||||
}
|
||||
rend.SetCrackAtlas(crackImg, crackRects)
|
||||
}
|
||||
// 实体贴图上传(渲染.md §5.3)
|
||||
if entityImg != nil {
|
||||
entityUV := make(map[string][4]float32, len(entityKeys))
|
||||
W, H := float32(entityImg.Bounds().Dx()), float32(entityImg.Bounds().Dy())
|
||||
for _, k := range entityKeys {
|
||||
rc := entityRectMap[k]
|
||||
entityUV[k] = [4]float32{
|
||||
float32(rc.Min.X) / W, float32(rc.Min.Y) / H,
|
||||
float32(rc.Max.X) / W, float32(rc.Max.Y) / H,
|
||||
}
|
||||
}
|
||||
rend.SetEntityAtlas(entityImg, entityUV)
|
||||
}
|
||||
|
||||
// 光影包(渲染.md §9:未安装 → 默认光影;坏包 → 回退默认)
|
||||
if sp, err := render.LoadShaderPack(am); err != nil {
|
||||
@@ -444,6 +693,33 @@ func main() {
|
||||
}
|
||||
hud.SetItemAtlas(itemTex, itemUV)
|
||||
}
|
||||
// 原版 HUD 精灵(UI还原.md §3.4:gui/sprites/hud/*)
|
||||
loadTex := func(key string) uint32 {
|
||||
p, ok := am.ResolveTexture(key)
|
||||
if !ok {
|
||||
return 0
|
||||
}
|
||||
f, err := os.Open(p)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
img, err := png.Decode(f)
|
||||
_ = f.Close()
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
return uiR.UploadImage(img)
|
||||
}
|
||||
hud.SetSprites(
|
||||
loadTex("gui/sprites/hud/crosshair"),
|
||||
loadTex("gui/sprites/hud/hotbar"),
|
||||
loadTex("gui/sprites/hud/hotbar_selection"),
|
||||
loadTex("gui/sprites/hud/heart/full"),
|
||||
loadTex("gui/sprites/hud/heart/half"),
|
||||
loadTex("gui/sprites/hud/heart/container"),
|
||||
loadTex("gui/sprites/hud/food_full"),
|
||||
loadTex("gui/sprites/hud/food_half"),
|
||||
)
|
||||
|
||||
// 界面屏幕(原版复刻:土质背景/原版按钮/中文标题,UI还原.md §2)
|
||||
lang, err := am.LoadLang(cfg.Client.Language)
|
||||
@@ -467,6 +743,8 @@ func main() {
|
||||
log: log,
|
||||
remotePlayers: make(map[uint32][3]float64),
|
||||
meshBuilder: mesh.NewBuilder(reg, uvFn),
|
||||
uv: uvFn,
|
||||
handSlot: -1,
|
||||
winW: cfg.Client.WindowWidth,
|
||||
winH: cfg.Client.WindowHeight,
|
||||
lastTime: time.Now(),
|
||||
|
||||
30
cmd/client/entities_gl.go
Normal file
30
cmd/client/entities_gl.go
Normal file
@@ -0,0 +1,30 @@
|
||||
//go:build gl
|
||||
|
||||
// 实体可视映射:实体类型 → 广告牌贴图键与高度(渲染.md §5.3)。
|
||||
package main
|
||||
|
||||
import "mc/internal/entity"
|
||||
|
||||
// entityVisual 返回实体的贴图键与身高(米)。
|
||||
// 动物按 ID 轮换四种外观(cow/pig/sheep/chicken)。
|
||||
func entityVisual(e *entity.Entity) (key string, height float32) {
|
||||
switch e.Kind {
|
||||
case entity.KindZombie:
|
||||
return "entity/zombie/zombie", 1.95
|
||||
case entity.KindVillager:
|
||||
return "entity/villager/villager", 1.95
|
||||
case entity.KindAnimal:
|
||||
switch e.ID % 4 {
|
||||
case 0:
|
||||
return "entity/cow/cow_temperate", 1.4
|
||||
case 1:
|
||||
return "entity/pig/pig_temperate", 0.9
|
||||
case 2:
|
||||
return "entity/sheep/sheep", 1.3
|
||||
default:
|
||||
return "entity/chicken/chicken_temperate", 0.7
|
||||
}
|
||||
default:
|
||||
return "", 0 // 掉落物等后置接入
|
||||
}
|
||||
}
|
||||
@@ -109,12 +109,6 @@ func (in *inputState) cursorCallback(_ *glfw.Window, x, y float64) {
|
||||
const sens = 0.0025
|
||||
in.dyaw += dx * sens
|
||||
in.dpitch -= dy * sens
|
||||
if in.dpitch > math.Pi/2-0.01 {
|
||||
in.dpitch = math.Pi/2 - 0.01
|
||||
}
|
||||
if in.dpitch < -math.Pi/2+0.01 {
|
||||
in.dpitch = -math.Pi/2 + 0.01
|
||||
}
|
||||
}
|
||||
|
||||
// scrollCallback 滚轮切热键栏(UI与输入.md §6)。
|
||||
@@ -131,12 +125,21 @@ func (in *inputState) apply(s *game.Session) {
|
||||
cam := s.Cam
|
||||
p := s.Player
|
||||
|
||||
// 视角
|
||||
// 视角(俯仰钳制在相机上,而非增量上——旧实现钳增量,抬头可翻转超过 ±90°)
|
||||
if in.dyaw != 0 || in.dpitch != 0 {
|
||||
cam.Yaw += in.dyaw
|
||||
cam.Pitch += in.dpitch
|
||||
in.dyaw, in.dpitch = 0, 0
|
||||
const maxPitch = math.Pi/2 - 0.01
|
||||
if cam.Pitch > maxPitch {
|
||||
cam.Pitch = maxPitch
|
||||
}
|
||||
if cam.Pitch < -maxPitch {
|
||||
cam.Pitch = -maxPitch
|
||||
}
|
||||
}
|
||||
// 游戏中左键=破坏,不是 UI 点击:清除遗留点击状态,防止暂停菜单收到过期点击
|
||||
in.clicked = false
|
||||
|
||||
// 水平移动(物理与碰撞.md §3:走/冲刺/潜行)
|
||||
f := cam.Forward()
|
||||
|
||||
@@ -24,10 +24,17 @@ var cjkPaths = []string{
|
||||
}
|
||||
|
||||
// loadCJK 加载系统 CJK 字体(失败返回 nil → 中文标签退化不显示)。
|
||||
func loadCJK() *cjkfont.Renderer {
|
||||
// 按字号缓存:不同字号(标题 96 / splash 24 / 按钮 18)需要各自的光栅化实例。
|
||||
var cjkCache = map[float64]*cjkfont.Renderer{}
|
||||
|
||||
func loadCJK(size float64) *cjkfont.Renderer {
|
||||
if r, ok := cjkCache[size]; ok {
|
||||
return r
|
||||
}
|
||||
for _, p := range cjkPaths {
|
||||
if _, err := os.Stat(p); err == nil {
|
||||
if r, err := cjkfont.Load(p, 24); err == nil {
|
||||
if r, err := cjkfont.Load(p, size); err == nil {
|
||||
cjkCache[size] = r
|
||||
return r
|
||||
}
|
||||
}
|
||||
@@ -37,8 +44,6 @@ func loadCJK() *cjkfont.Renderer {
|
||||
|
||||
// buildScreens 构建主菜单与暂停菜单(原版布局与控件)。
|
||||
func buildScreens(r *ui.Renderer, am *assets.Manager, lang *assets.Lang) (*ui.TitleScreen, *ui.PauseScreen) {
|
||||
cjk := loadCJK()
|
||||
|
||||
// 原版按钮控件(widget/button.png 200×20 与悬停高亮)
|
||||
loadPng := func(key string) (uint32, bool) {
|
||||
p, ok := am.ResolveTexture(key)
|
||||
@@ -62,8 +67,9 @@ func buildScreens(r *ui.Renderer, am *assets.Manager, lang *assets.Lang) (*ui.Ti
|
||||
// 土质背景:16px dirt 平铺 1280×720(原版菜单背景质感)
|
||||
bgTex := tileBackground(r, am)
|
||||
|
||||
// 中文文本光栅化
|
||||
// 中文文本光栅化(按请求字号取缓存渲染器)
|
||||
text := func(s string, size float64, c color.RGBA) (uint32, float32, float32) {
|
||||
cjk := loadCJK(size)
|
||||
if cjk == nil || s == "" {
|
||||
return 0, 0, 0
|
||||
}
|
||||
@@ -94,6 +100,13 @@ func buildScreens(r *ui.Renderer, am *assets.Manager, lang *assets.Lang) (*ui.Ti
|
||||
Version: "年糕历险记 v0.1.0",
|
||||
Copyright: "Copyright NianGao Studio. 请勿分发!",
|
||||
}
|
||||
// 版本/版权角标:中文走 CJK 光栅(ASCII 位图字体缺中文字形)
|
||||
if vTex, vw, vh := tr("menu.version", "年糕历险记 v0.1.0", 12, color.RGBA{220, 220, 220, 255}); vTex != 0 {
|
||||
title.VersionTex, title.VersionW, title.VersionH = vTex, vw, vh
|
||||
}
|
||||
if cTex, cw, ch := text("Copyright NianGao Studio. 请勿分发!", 12, color.RGBA{220, 220, 220, 255}); cTex != 0 {
|
||||
title.CopyrightTex, title.CopyrightW, title.CopyrightH = cTex, cw, ch
|
||||
}
|
||||
title.Buttons = []ui.Button{
|
||||
mkBtn("menu.singleplayer", "单人游戏", "play", 390),
|
||||
mkBtn("menu.multiplayer", "多人游戏", "multi", 414),
|
||||
|
||||
70
cmd/client/tint_gl.go
Normal file
70
cmd/client/tint_gl.go
Normal file
@@ -0,0 +1,70 @@
|
||||
//go:build gl
|
||||
|
||||
// 图集植被染色(渲染.md §4.1):
|
||||
// 原版材质包的草顶/树叶纹理是灰度图,绿色来自生物群系染色表
|
||||
// (textures/colormap/grass.png、foliage.png)。
|
||||
// MVP:取染色表中心色(平原绿)对图集内对应纹理乘色;按群系逐顶点着色后置。
|
||||
package main
|
||||
|
||||
import (
|
||||
"image"
|
||||
"image/color"
|
||||
"image/draw"
|
||||
"image/png"
|
||||
"os"
|
||||
|
||||
"mc/internal/assets"
|
||||
)
|
||||
|
||||
// applyAtlasTint 给图集内带染色标记的纹理乘上染色表中心色。
|
||||
// 返回新图集图像(原地复制后修改);染色表缺失的键保持原样。
|
||||
func applyAtlasTint(src image.Image, uv map[string]image.Rectangle, am *assets.Manager, tinted map[string][]string) image.Image {
|
||||
b := src.Bounds()
|
||||
dst := image.NewRGBA(image.Rect(0, 0, b.Dx(), b.Dy()))
|
||||
draw.Draw(dst, dst.Bounds(), src, b.Min, draw.Src)
|
||||
|
||||
for tintName, keys := range tinted {
|
||||
tr, tg, tb, ok := colormapCenter(am, tintName)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
for _, key := range keys {
|
||||
rect, found := uv[key]
|
||||
if !found {
|
||||
continue
|
||||
}
|
||||
for y := rect.Min.Y; y < rect.Max.Y; y++ {
|
||||
for x := rect.Min.X; x < rect.Max.X; x++ {
|
||||
pr, pg, pb, pa := src.At(x, y).RGBA()
|
||||
dst.SetRGBA(x, y, color.RGBA{
|
||||
R: uint8(uint32(uint8(pr>>8)) * tr / 255),
|
||||
G: uint8(uint32(uint8(pg>>8)) * tg / 255),
|
||||
B: uint8(uint32(uint8(pb>>8)) * tb / 255),
|
||||
A: uint8(pa >> 8),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
// colormapCenter 读取染色表中心像素(0–255)。
|
||||
func colormapCenter(am *assets.Manager, name string) (r, g, b uint32, ok bool) {
|
||||
p, found := am.ResolveTexture("colormap/" + name)
|
||||
if !found {
|
||||
return 0, 0, 0, false
|
||||
}
|
||||
f, err := os.Open(p)
|
||||
if err != nil {
|
||||
return 0, 0, 0, false
|
||||
}
|
||||
defer f.Close()
|
||||
img, err := png.Decode(f)
|
||||
if err != nil {
|
||||
return 0, 0, 0, false
|
||||
}
|
||||
cb := img.Bounds()
|
||||
r, g, b, _ = img.At(cb.Min.X+cb.Dx()/2, cb.Min.Y+cb.Dy()/2).RGBA()
|
||||
return r >> 8, g >> 8, b >> 8, true
|
||||
}
|
||||
2
go.mod
2
go.mod
@@ -11,6 +11,7 @@ require (
|
||||
github.com/klauspost/compress v1.19.2
|
||||
github.com/ojrac/opensimplex-go v1.0.2
|
||||
github.com/yuin/gopher-lua v1.1.2
|
||||
golang.org/x/image v0.45.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
@@ -19,7 +20,6 @@ require (
|
||||
github.com/hajimehoshi/oto v0.7.1 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8 // indirect
|
||||
golang.org/x/image v0.45.0 // indirect
|
||||
golang.org/x/mobile v0.0.0-20190415191353-3e0bab5405d6 // indirect
|
||||
golang.org/x/sys v0.47.0 // indirect
|
||||
golang.org/x/text v0.41.0 // indirect
|
||||
|
||||
@@ -42,6 +42,7 @@ type Def struct {
|
||||
RequiredTier int `json:"required_tier"`
|
||||
Drops string `json:"drops"`
|
||||
Textures map[string]string `json:"textures"`
|
||||
Tint string `json:"tint"` // 植被染色表:grass/foliage(渲染.md §4.1)
|
||||
}
|
||||
|
||||
// Registry 方块注册表。
|
||||
@@ -140,3 +141,23 @@ func (r *Registry) Texture(s State, face string) string {
|
||||
}
|
||||
return "block/" + d.Name
|
||||
}
|
||||
|
||||
// TintTextureKeys 返回「染色表名 → 涉及纹理键」:图集构建后按染色表给这些
|
||||
// 纹理乘色(渲染.md §4.1 植被染色)。无染色标记的方块不出现。
|
||||
func (r *Registry) TintTextureKeys() map[string][]string {
|
||||
out := make(map[string][]string)
|
||||
for _, d := range r.byID {
|
||||
if d.ID == 0 || d.Tint == "" {
|
||||
continue
|
||||
}
|
||||
seen := make(map[string]struct{})
|
||||
for _, k := range d.Textures {
|
||||
if _, dup := seen[k]; dup {
|
||||
continue
|
||||
}
|
||||
seen[k] = struct{}{}
|
||||
out[d.Tint] = append(out[d.Tint], k)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
@@ -129,6 +129,13 @@ func (s *Session) BreakProgress() float64 { return s.breakProg }
|
||||
// BreakTarget 当前破坏目标(渲染选择框/裂纹用)。
|
||||
func (s *Session) BreakTarget() raycast.Hit { return s.breakTarget }
|
||||
|
||||
// SetBreakVisual 直接设置破坏进度显示状态(客户端渲染层裂纹叠加/开发测试用;
|
||||
// 不修改世界数据,不驱动 accumulateBreak)。
|
||||
func (s *Session) SetBreakVisual(x, y, z int32, prog float64) {
|
||||
s.breakTarget = raycast.Hit{X: x, Y: y, Z: z}
|
||||
s.breakProg = prog
|
||||
}
|
||||
|
||||
// breakSpeed 每秒破坏进度(方块交互与动画.md §3、挖矿与矿物.md §3 同公式)。
|
||||
func (s *Session) breakSpeed(def block.Def, tool item.Stack) float64 {
|
||||
speed := 1.0 // 空手
|
||||
|
||||
112
internal/render/entity_gl.go
Normal file
112
internal/render/entity_gl.go
Normal file
@@ -0,0 +1,112 @@
|
||||
//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"
|
||||
"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。
|
||||
func (r *Renderer) DrawBillboard(x, y, z float32, h, w float32, uv [4]float32) {
|
||||
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)
|
||||
// 相机基向量(视图矩阵列 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))
|
||||
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)
|
||||
}
|
||||
@@ -102,6 +102,14 @@ func (r *Renderer) DrawChunks(get func(i int) (int32, int32), n int) {
|
||||
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)
|
||||
|
||||
231
internal/render/overlay_gl.go
Normal file
231
internal/render/overlay_gl.go
Normal file
@@ -0,0 +1,231 @@
|
||||
//go:build gl
|
||||
|
||||
// 覆盖层渲染:破坏裂纹叠加(destroy_stage_0..9)与第一人称手持方块
|
||||
// (方块交互与动画.md §4、渲染.md §5.2)。
|
||||
// 与区块网格共用顶点格式(pos+uv+light)与着色器,通过 uOffset 平移定位。
|
||||
package render
|
||||
|
||||
import (
|
||||
"image"
|
||||
"unsafe"
|
||||
|
||||
"mc/internal/mesh"
|
||||
|
||||
"github.com/go-gl/gl/v3.3-core/gl"
|
||||
)
|
||||
|
||||
// cubeFaces 单位立方体 6 面角顺序(与 mesh.faceDef 一致,CW 绕序为正面)。
|
||||
var cubeFaces = [6][4][3]float32{
|
||||
{{0, 1, 0}, {1, 1, 0}, {1, 1, 1}, {0, 1, 1}}, // 顶
|
||||
{{0, 0, 1}, {1, 0, 1}, {1, 0, 0}, {0, 0, 0}}, // 底
|
||||
{{1, 0, 0}, {1, 0, 1}, {1, 1, 1}, {1, 1, 0}}, // 东(+x)
|
||||
{{0, 0, 1}, {0, 0, 0}, {0, 1, 0}, {0, 1, 1}}, // 西(-x)
|
||||
{{1, 0, 1}, {0, 0, 1}, {0, 1, 1}, {1, 1, 1}}, // 南(+z)
|
||||
{{0, 0, 0}, {1, 0, 0}, {1, 1, 0}, {0, 1, 0}}, // 北(-z)
|
||||
}
|
||||
|
||||
// cubeVerts 按六面 UV 构建单位立方体顶点(mesh.Vertex 格式:pos+uv+light 字节,
|
||||
// 光照满亮——覆盖层不受世界光照影响)。inflate 沿角外扩(裂纹盒防 z-fighting)。
|
||||
func cubeVerts(uvs [6][4]float32, inflate float32) []mesh.Vertex {
|
||||
var verts []mesh.Vertex
|
||||
for fi, face := range cubeFaces {
|
||||
u0, v0, u1, v1 := uvs[fi][0], uvs[fi][1], uvs[fi][2], uvs[fi][3]
|
||||
for _, c := range face {
|
||||
// 角 UV 权重(同 mesh.cornerUV)
|
||||
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(v 翻转)
|
||||
cu, cv = c[2], 1-c[1]
|
||||
default: // 南/北:x→u,y→v(v 翻转)
|
||||
cu, cv = c[0], 1-c[1]
|
||||
}
|
||||
verts = append(verts, mesh.Vertex{
|
||||
X: (c[0]-0.5)*(1+inflate) + 0.5,
|
||||
Y: (c[1]-0.5)*(1+inflate) + 0.5,
|
||||
Z: (c[2]-0.5)*(1+inflate) + 0.5,
|
||||
U: u0 + (u1-u0)*cu, V: v0 + (v1-v0)*cv,
|
||||
Sky: 15, Block: 15,
|
||||
})
|
||||
}
|
||||
}
|
||||
return verts
|
||||
}
|
||||
|
||||
// cubeIndices 24 顶点 → 36 索引(每面 2 三角形)。
|
||||
func cubeIndices() []uint32 {
|
||||
idx := make([]uint32, 0, 36)
|
||||
for base := 0; base < 24; base += 4 {
|
||||
b := uint32(base)
|
||||
idx = append(idx, b, b+1, b+2, b+2, b+3, b)
|
||||
}
|
||||
return idx
|
||||
}
|
||||
|
||||
// uploadMesh 上传顶点/索引并绑定顶点属性(chunk 顶点布局,渲染.md §5)。
|
||||
func uploadMesh(verts []mesh.Vertex, idx []uint32) (vao, vbo, ibo uint32) {
|
||||
gl.GenVertexArrays(1, &vao)
|
||||
gl.GenBuffers(1, &vbo)
|
||||
gl.GenBuffers(1, &ibo)
|
||||
gl.BindVertexArray(vao)
|
||||
gl.BindBuffer(gl.ARRAY_BUFFER, vbo)
|
||||
gl.BufferData(gl.ARRAY_BUFFER, len(verts)*int(unsafe.Sizeof(mesh.Vertex{})), gl.Ptr(verts), gl.STATIC_DRAW)
|
||||
gl.BindBuffer(gl.ELEMENT_ARRAY_BUFFER, ibo)
|
||||
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)
|
||||
return
|
||||
}
|
||||
|
||||
// buildCubeVAO 构建立方体 VAO(顶点格式与 chunk 一致:pos+uv+light)。
|
||||
// uvs 六面 UV(顺序同 cubeFaces);inflate 沿角外扩;立方体锚点为原点,边长为 1。
|
||||
func buildCubeVAO(uvs [6][4]float32, inflate float32) uint32 {
|
||||
vao, _, _ := uploadMesh(cubeVerts(uvs, inflate), cubeIndices())
|
||||
return vao
|
||||
}
|
||||
|
||||
// SetCrackAtlas 上传裂纹图集(10 阶段 destroy_stage)并记录各阶段 UV。
|
||||
// img 为调用方打包好的图集,rects[i] 为阶段 i 的像素矩形(图集坐标系)。
|
||||
func (r *Renderer) SetCrackAtlas(img image.Image, rects [10]image.Rectangle) {
|
||||
if r.crackTex == 0 {
|
||||
gl.GenTextures(1, &r.crackTex)
|
||||
}
|
||||
gl.BindTexture(gl.TEXTURE_2D, r.crackTex)
|
||||
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)
|
||||
|
||||
W, H := float32(b.Dx()), float32(b.Dy())
|
||||
const inset = 0.5 // 防渗色(渲染.md §4)
|
||||
for i, rc := range rects {
|
||||
r.crackUVs[i] = [4]float32{
|
||||
(float32(rc.Min.X) + inset) / W, (float32(rc.Min.Y) + inset) / H,
|
||||
(float32(rc.Max.X) - inset) / W, (float32(rc.Max.Y) - inset) / H,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DrawCrackOverlay 在目标方块 (x,y,z) 叠加 stage 阶段裂纹盒
|
||||
// (方块交互与动画.md §4:0–9,六面同图,盒体略外扩防 z-fighting)。
|
||||
func (r *Renderer) DrawCrackOverlay(x, y, z float32, stage int) {
|
||||
if r.crackTex == 0 {
|
||||
return
|
||||
}
|
||||
if stage < 0 {
|
||||
stage = 0
|
||||
}
|
||||
if stage > 9 {
|
||||
stage = 9
|
||||
}
|
||||
if r.crackCubes[stage] == 0 {
|
||||
uv := r.crackUVs[stage]
|
||||
r.crackCubes[stage] = buildCubeVAO([6][4]float32{uv, uv, uv, uv, uv, uv}, 0.004)
|
||||
}
|
||||
|
||||
gl.UseProgram(r.prog)
|
||||
gl.ActiveTexture(gl.TEXTURE0)
|
||||
gl.BindTexture(gl.TEXTURE_2D, r.crackTex)
|
||||
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)
|
||||
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)
|
||||
gl.Uniform2f(uvMaxLoc, 1, 1)
|
||||
|
||||
gl.Enable(gl.POLYGON_OFFSET_FILL)
|
||||
gl.PolygonOffset(-1, -1)
|
||||
gl.BindVertexArray(r.crackCubes[stage])
|
||||
gl.DrawElements(gl.TRIANGLES, 36, gl.UNSIGNED_INT, gl.PtrOffset(0))
|
||||
gl.BindVertexArray(0)
|
||||
gl.Disable(gl.POLYGON_OFFSET_FILL)
|
||||
gl.Uniform3f(offLoc, 0, 0, 0) // 复位平移(后续绘制依赖 0 偏移)
|
||||
}
|
||||
|
||||
// SetHandCube 上传/更新手持方块立方体(六面 UV 顺序同 cubeFaces)。
|
||||
// pos/scale 为视图空间下的立方体中心与边长(渲染.md §5.2)。
|
||||
func (r *Renderer) SetHandCube(uvs [6][4]float32, pos [3]float32, scale float32) {
|
||||
r.ClearHand()
|
||||
// 直接按视图空间坐标构建顶点(顶点含 pos 偏移)
|
||||
var verts []mesh.Vertex
|
||||
for fi, face := range cubeFaces {
|
||||
u0, v0, u1, v1 := uvs[fi][0], uvs[fi][1], uvs[fi][2], uvs[fi][3]
|
||||
for _, c := range face {
|
||||
var cu, cv float32
|
||||
switch fi {
|
||||
case 0, 1:
|
||||
cu, cv = c[0], c[2]
|
||||
case 2, 3:
|
||||
cu, cv = c[2], 1-c[1]
|
||||
default:
|
||||
cu, cv = c[0], 1-c[1]
|
||||
}
|
||||
verts = append(verts, mesh.Vertex{
|
||||
X: pos[0] + (c[0]-0.5)*scale,
|
||||
Y: pos[1] + (c[1]-0.5)*scale,
|
||||
Z: pos[2] + (c[2]-0.5)*scale,
|
||||
U: u0 + (u1-u0)*cu, V: v0 + (v1-v0)*cv,
|
||||
Sky: 15, Block: 15,
|
||||
})
|
||||
}
|
||||
}
|
||||
r.handVao, r.handVbo, r.handIbo = uploadMesh(verts, cubeIndices())
|
||||
}
|
||||
|
||||
// ClearHand 释放手持方块网格(手持物品为空时调用)。
|
||||
func (r *Renderer) ClearHand() {
|
||||
if r.handVao != 0 {
|
||||
gl.DeleteVertexArrays(1, &r.handVao)
|
||||
r.handVao = 0
|
||||
}
|
||||
if r.handVbo != 0 {
|
||||
gl.DeleteBuffers(1, &r.handVbo)
|
||||
r.handVbo = 0
|
||||
}
|
||||
if r.handIbo != 0 {
|
||||
gl.DeleteBuffers(1, &r.handIbo)
|
||||
r.handIbo = 0
|
||||
}
|
||||
}
|
||||
|
||||
// DrawHand 绘制手持方块(顶点已是视图空间,仅乘投影矩阵)。
|
||||
func (r *Renderer) DrawHand() {
|
||||
if r.handVao == 0 {
|
||||
return
|
||||
}
|
||||
gl.UseProgram(r.prog)
|
||||
gl.ActiveTexture(gl.TEXTURE0)
|
||||
gl.BindTexture(gl.TEXTURE_2D, r.atlas)
|
||||
vpLoc := gl.GetUniformLocation(r.prog, gl.Str("uViewProj\x00"))
|
||||
gl.UniformMatrix4fv(vpLoc, 1, false, &r.proj[0])
|
||||
offLoc := gl.GetUniformLocation(r.prog, gl.Str("uOffset\x00"))
|
||||
gl.Uniform3f(offLoc, 0, 0, 0)
|
||||
cutLoc := gl.GetUniformLocation(r.prog, gl.Str("uCutout\x00"))
|
||||
gl.Uniform1f(cutLoc, 0)
|
||||
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)
|
||||
gl.Uniform2f(uvMaxLoc, 1, 1)
|
||||
gl.BindVertexArray(r.handVao)
|
||||
gl.DrawElements(gl.TRIANGLES, 36, gl.UNSIGNED_INT, gl.PtrOffset(0))
|
||||
gl.BindVertexArray(0)
|
||||
}
|
||||
@@ -27,6 +27,19 @@ type Renderer struct {
|
||||
// 区块网格缓存:key → VAO/VBO/索引数
|
||||
chunks map[uint64]*chunkGL
|
||||
|
||||
// 裂纹覆盖层(方块交互与动画.md §4)
|
||||
crackTex uint32
|
||||
crackUVs [10][4]float32
|
||||
crackCubes [10]uint32
|
||||
|
||||
// 第一人称手持方块(渲染.md §5.2)
|
||||
handVao, handVbo, handIbo uint32
|
||||
|
||||
// 实体广告牌(渲染.md §5.3)
|
||||
entityTex uint32
|
||||
entityUV map[string][4]float32
|
||||
billVao, billVbo, billIbo uint32
|
||||
|
||||
proj [16]float32
|
||||
view [16]float32
|
||||
}
|
||||
@@ -217,11 +230,21 @@ 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;
|
||||
uniform vec3 uOffset; // 覆盖层/广告牌锚点平移(区块绘制时为 0)
|
||||
uniform vec3 uCamRight; // 相机右向量(世界系,广告牌展开用)
|
||||
uniform vec3 uCamUp; // 相机上向量(世界系)
|
||||
uniform vec2 uBill; // 广告牌缩放:x=右 y=上;y=0 时走普通路径
|
||||
uniform vec2 uUVMin; // UV 重映射下限(区块/普通立方体为 0,0)
|
||||
uniform vec2 uUVMax; // UV 重映射上限(区块/普通立方体为 1,1)
|
||||
out vec2 vUV;
|
||||
out vec2 vLight;
|
||||
void main() {
|
||||
gl_Position = uViewProj * vec4(aPos, 1.0);
|
||||
vUV = aUV;
|
||||
vec3 pos = aPos + uOffset;
|
||||
if (uBill.y > 0.001) {
|
||||
pos = uOffset + vec3(aPos.x * uCamRight * uBill.x + aPos.y * uCamUp * uBill.y);
|
||||
}
|
||||
gl_Position = uViewProj * vec4(pos, 1.0);
|
||||
vUV = uUVMin + aUV * (uUVMax - uUVMin);
|
||||
vLight = aLight;
|
||||
}
|
||||
`
|
||||
|
||||
@@ -19,6 +19,17 @@ type HUD struct {
|
||||
itemTex uint32
|
||||
itemUV map[string][4]float32
|
||||
|
||||
// 原版 HUD 精灵(UI还原.md §3.4:gui/sprites/hud/*)
|
||||
crossTex uint32 // crosshair.png 15×15
|
||||
hotbarTex uint32 // hotbar.png 182×22
|
||||
hotbarSel uint32 // hotbar_selection.png 24×24
|
||||
heartFull uint32 // heart/full.png 9×9
|
||||
heartHalf uint32 // heart/half.png
|
||||
heartCont uint32 // heart/container.png
|
||||
foodFull uint32 // food_full.png 9×9
|
||||
foodHalf uint32 // food_half.png
|
||||
haveSprites bool
|
||||
|
||||
// 逻辑分辨率与缩放(UI还原.md §3.1:427×240 基准 × 整数缩放)
|
||||
baseW, baseH float32
|
||||
scale float32
|
||||
@@ -38,61 +49,119 @@ func (h *HUD) SetItemAtlas(tex uint32, uv map[string][4]float32) {
|
||||
h.itemUV = uv
|
||||
}
|
||||
|
||||
// SetSprites 设置原版 HUD 精灵纹理(跨 package 上传后注入)。
|
||||
func (h *HUD) SetSprites(cross, hotbar, hotbarSel, heartFull, heartHalf, heartCont, foodFull, foodHalf uint32) {
|
||||
h.crossTex, h.hotbarTex, h.hotbarSel = cross, hotbar, hotbarSel
|
||||
h.heartFull, h.heartHalf, h.heartCont = heartFull, heartHalf, heartCont
|
||||
h.foodFull, h.foodHalf = foodFull, foodHalf
|
||||
h.haveSprites = cross != 0
|
||||
}
|
||||
|
||||
// Draw 每帧绘制 HUD(调用方已 Begin)。
|
||||
func (h *HUD) Draw(r *Renderer) {
|
||||
w, hgt := float32(r.w), float32(r.h)
|
||||
s := h.scale
|
||||
white := [4]float32{1, 1, 1, 1}
|
||||
|
||||
// 准星(UI还原.md §3.4:屏幕正中 15×15 十字)
|
||||
// 准星(原版 crosshair.png 15×15,UI还原.md §3.4)
|
||||
cx, cy := w/2, hgt/2
|
||||
r.DrawRect(cx-1*s, cy-8*s, 2*s, 16*s, white)
|
||||
r.DrawRect(cx-8*s, cy-1*s, 16*s, 2*s, white)
|
||||
if h.haveSprites {
|
||||
h.blit(r, h.crossTex, cx-7.5*s, cy-7.5*s, 15*s, 15*s)
|
||||
} else {
|
||||
white := [4]float32{1, 1, 1, 1}
|
||||
r.DrawRect(cx-1*s, cy-8*s, 2*s, 16*s, white)
|
||||
r.DrawRect(cx-8*s, cy-1*s, 16*s, 2*s, white)
|
||||
}
|
||||
|
||||
// 热键栏(UI还原.md §3.4:182×22,底部居中)
|
||||
// 热键栏(原版 hotbar.png 182×22,底部居中,UI还原.md §3.4)
|
||||
barW, barH := 182*s, 22*s
|
||||
bx, by := cx-barW/2, hgt-barH
|
||||
r.DrawRect(bx, by, barW, barH, [4]float32{0, 0, 0, 0.5})
|
||||
if h.haveSprites {
|
||||
h.blit(r, h.hotbarTex, bx, by, barW, barH)
|
||||
} else {
|
||||
r.DrawRect(bx, by, barW, barH, [4]float32{0, 0, 0, 0.5})
|
||||
}
|
||||
for i := 0; i < 9; i++ {
|
||||
sx := bx + float32(i)*20*s + 1*s
|
||||
sy := by + 3*s
|
||||
if i == h.sess.Inv.Selected {
|
||||
r.DrawRect(sx-2*s, sy-2*s, 20*s, 20*s, [4]float32{1, 1, 1, 0.9}) // 选中高亮框
|
||||
if h.haveSprites {
|
||||
h.blit(r, h.hotbarSel, bx+float32(i)*20*s-2*s, by-1*s, 24*s, 24*s)
|
||||
} else {
|
||||
r.DrawRect(bx+float32(i)*20*s+1*s, by+3*s, 20*s, 20*s, [4]float32{1, 1, 1, 0.9})
|
||||
}
|
||||
}
|
||||
h.drawItemIcon(r, h.sess.Inv.Slots[i], sx, sy)
|
||||
h.drawItemIcon(r, h.sess.Inv.Slots[i], bx+float32(i)*20*s+3*s, by+3*s)
|
||||
}
|
||||
|
||||
// 生命:10 心(每心 9×9,屏幕左下,UI还原.md §3.4)
|
||||
hearts := int(20 / 2)
|
||||
// 生命:10 心(原版 9×9 图标,屏幕左下,UI还原.md §3.4 布局:
|
||||
// 饥饿行 y=h-39,生命行在其上 y=h-49)
|
||||
hp := h.sess.PlayerHealth()
|
||||
hx, hy := w/2-91*s, hgt-30*s
|
||||
for i := 0; i < hearts; i++ {
|
||||
color := [4]float32{0.2, 0.2, 0.2, 0.6} // 空
|
||||
if i < hp/2 {
|
||||
color = [4]float32{1, 0.1, 0.1, 1} // 满
|
||||
}
|
||||
r.DrawRect(hx+float32(i%10)*8*s, hy+float32(i/10)*9*s, 7*s, 7*s, color)
|
||||
}
|
||||
|
||||
// 饥饿:10 鸡腿(右侧偏移)
|
||||
hx := w/2 - 91*s
|
||||
hy := hgt - 49*s
|
||||
for i := 0; i < 10; i++ {
|
||||
r.DrawRect(w/2-91*s+100*s+float32(i%10)*8*s, hy+float32(i/10)*9*s, 7*s, 7*s, [4]float32{0.6, 0.4, 0.15, 1})
|
||||
x := hx + float32(i)*8*s
|
||||
// 底:容器(空槽)
|
||||
if h.haveSprites {
|
||||
h.blit(r, h.heartCont, x, hy, 9*s, 9*s)
|
||||
}
|
||||
switch {
|
||||
case hp >= (i+1)*2: // 满心
|
||||
if h.haveSprites {
|
||||
h.blit(r, h.heartFull, x, hy, 9*s, 9*s)
|
||||
} else {
|
||||
r.DrawRect(x, hy, 7*s, 7*s, [4]float32{1, 0.1, 0.1, 1})
|
||||
}
|
||||
case hp == i*2+1: // 半心
|
||||
if h.haveSprites {
|
||||
h.blit(r, h.heartHalf, x, hy, 9*s, 9*s)
|
||||
} else {
|
||||
r.DrawRect(x, hy, 3.5*s, 7*s, [4]float32{1, 0.1, 0.1, 1})
|
||||
}
|
||||
default: // 空
|
||||
if !h.haveSprites {
|
||||
r.DrawRect(x, hy, 7*s, 7*s, [4]float32{0.2, 0.2, 0.2, 0.6})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 破坏进度(方块交互与动画.md §4:HUD 中央进度)
|
||||
if prog := h.sess.BreakProgress(); prog > 0 {
|
||||
r.DrawRect(cx-40*s, cy+20*s, 80*s, 4*s, [4]float32{0, 0, 0, 0.7})
|
||||
r.DrawRect(cx-40*s, cy+20*s, 80*s*float32(prog), 4*s, [4]float32{1, 1, 1, 0.9})
|
||||
// 饥饿:10 鸡腿(原版 food_full,生命行下方)
|
||||
food := 20 // 饥饿值(饥饿系统后置,恒定 20)
|
||||
fy := hgt - 39*s
|
||||
for i := 0; i < 10; i++ {
|
||||
x := hx + float32(i)*8*s
|
||||
switch {
|
||||
case food >= (i+1)*2:
|
||||
if h.haveSprites {
|
||||
h.blit(r, h.foodFull, x, fy, 9*s, 9*s)
|
||||
} else {
|
||||
r.DrawRect(x, fy, 7*s, 7*s, [4]float32{0.6, 0.4, 0.15, 1})
|
||||
}
|
||||
case food == i*2+1:
|
||||
if h.haveSprites {
|
||||
h.blit(r, h.foodHalf, x, fy, 9*s, 9*s)
|
||||
} else {
|
||||
r.DrawRect(x, fy, 3.5*s, 7*s, [4]float32{0.6, 0.4, 0.15, 1})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 破坏进度(方块交互与动画.md §4:3D 裂纹由渲染层叠加,HUD 不画进度条)
|
||||
|
||||
// 坐标调试(后置移除)
|
||||
r.DrawText(w/2, hgt-40*s, fmt.Sprintf("XYZ %.1f / %.1f / %.1f", h.sess.Player.Pos[0], h.sess.Player.Pos[1], h.sess.Player.Pos[2]), 1*s, white)
|
||||
r.DrawText(w/2, hgt-70*s, fmt.Sprintf("XYZ %.1f / %.1f / %.1f", h.sess.Player.Pos[0], h.sess.Player.Pos[1], h.sess.Player.Pos[2]), 1*s, [4]float32{1, 1, 1, 1})
|
||||
// 联机在线人数(多人同步.md §3)
|
||||
if h.OnlineCount != nil {
|
||||
r.DrawText(w/2, hgt-56*s, fmt.Sprintf("在线玩家: %d", h.OnlineCount()), 1*s, white)
|
||||
r.DrawText(w/2, hgt-82*s, fmt.Sprintf("在线玩家: %d", h.OnlineCount()), 1*s, [4]float32{1, 1, 1, 1})
|
||||
}
|
||||
}
|
||||
|
||||
// blit 以纹理原比例绘制一个四边形(UI 精灵)。
|
||||
func (h *HUD) blit(r *Renderer, tex uint32, x, y, w, hgt float32) {
|
||||
if tex == 0 {
|
||||
return
|
||||
}
|
||||
r.pushQuad(quad{x: x, y: y, w: w, h: hgt, u0: 0, v0: 0, u1: 1, v1: 1,
|
||||
r: 1, g: 1, b: 1, a: 1, tex: tex})
|
||||
}
|
||||
|
||||
// drawItemIcon 在槽位绘制物品图标 + 数量。
|
||||
func (h *HUD) drawItemIcon(r *Renderer, stack item.Stack, x, y float32) {
|
||||
s := h.scale
|
||||
@@ -104,7 +173,7 @@ func (h *HUD) drawItemIcon(r *Renderer, stack item.Stack, x, y float32) {
|
||||
return
|
||||
}
|
||||
if uv, found := h.itemUV[def.Texture]; found && h.itemTex != 0 {
|
||||
r.pushQuad(quad{x: x + 2*s, y: y + 2*s, w: 16 * s, h: 16 * s,
|
||||
r.pushQuad(quad{x: x, y: y, w: 16 * s, h: 16 * s,
|
||||
u0: uv[0], v0: uv[1], u1: uv[2], v1: uv[3],
|
||||
r: 1, g: 1, b: 1, a: 1, tex: h.itemTex})
|
||||
}
|
||||
|
||||
@@ -27,8 +27,14 @@ type TitleScreen struct {
|
||||
BtnHover uint32 // 悬停高亮(button_highlighted.png)
|
||||
Buttons []Button
|
||||
|
||||
Version string // 左下角版本(原版角标)
|
||||
Copyright string // 右下角版权
|
||||
Version string // 左下角版本(原版角标)
|
||||
Copyright string // 右下角版权
|
||||
VersionTex uint32 // 版本/版权 CJK 光栅纹理(中文字形)
|
||||
VersionW float32
|
||||
VersionH float32
|
||||
CopyrightTex uint32
|
||||
CopyrightW float32
|
||||
CopyrightH float32
|
||||
}
|
||||
|
||||
// HitTest 命中检测(按钮区域)。
|
||||
@@ -78,11 +84,17 @@ func (s *TitleScreen) Draw(r *Renderer, mx, my float32) {
|
||||
u0: 0, v0: 0, u1: 1, v1: 1, r: 1, g: 1, b: 1, a: 1, tex: b.Tex})
|
||||
}
|
||||
}
|
||||
// 版本角标(左下)与版权(右下,原版样式)
|
||||
if s.Version != "" {
|
||||
// 版本角标(左下)与版权(右下,原版样式;中文用预光栅纹理)
|
||||
if s.VersionTex != 0 {
|
||||
r.pushQuad(quad{x: 4, y: h - s.VersionH - 4, w: s.VersionW, h: s.VersionH,
|
||||
u0: 0, v0: 0, u1: 1, v1: 1, r: 1, g: 1, b: 1, a: 1, tex: s.VersionTex})
|
||||
} else if s.Version != "" {
|
||||
r.DrawText(8, h-12, s.Version, 1, [4]float32{1, 1, 1, 1})
|
||||
}
|
||||
if s.Copyright != "" {
|
||||
if s.CopyrightTex != 0 {
|
||||
r.pushQuad(quad{x: w - s.CopyrightW - 4, y: h - s.CopyrightH - 4, w: s.CopyrightW, h: s.CopyrightH,
|
||||
u0: 0, v0: 0, u1: 1, v1: 1, r: 1, g: 1, b: 1, a: 1, tex: s.CopyrightTex})
|
||||
} else if s.Copyright != "" {
|
||||
r.DrawText(w-r.TextWidth(s.Copyright, 1)-8, h-12, s.Copyright, 1, [4]float32{1, 1, 1, 1})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user