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:
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user