feat(render): 纹理图集构建与区块网格构建(面剔除/三通道/光照顶点)
This commit is contained in:
82
internal/atlas/atlas.go
Normal file
82
internal/atlas/atlas.go
Normal file
@@ -0,0 +1,82 @@
|
||||
// Package atlas 纹理图集构建:把纹理键列表打包成单张图(渲染.md §4)。
|
||||
//
|
||||
// 所有纹理统一按 tileSize 采样(最近邻缩放,保持像素风),
|
||||
// 返回图集图像与「纹理键 → 像素矩形」映射,供网格构建换算 UV。
|
||||
package atlas
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image"
|
||||
"image/draw"
|
||||
"image/png"
|
||||
"os"
|
||||
)
|
||||
|
||||
// Build 打包纹理键列表为图集。
|
||||
// resolve:纹理键 → PNG 文件路径;缺失纹理跳过(键不进入 uv 表,运行时用占位纹理)。
|
||||
// 列数取「大于等于根号的整数」,使图集接近正方形(GPU 友好)。
|
||||
func Build(resolve func(key string) (string, bool), keys []string, tileSize int) (image.Image, map[string]image.Rectangle, error) {
|
||||
if len(keys) == 0 {
|
||||
return nil, nil, fmt.Errorf("atlas.Build: 纹理列表为空")
|
||||
}
|
||||
if tileSize <= 0 {
|
||||
tileSize = 16
|
||||
}
|
||||
cols := 1
|
||||
for cols*cols < len(keys) {
|
||||
cols++
|
||||
}
|
||||
rows := (len(keys) + cols - 1) / cols
|
||||
img := image.NewRGBA(image.Rect(0, 0, cols*tileSize, rows*tileSize))
|
||||
uv := make(map[string]image.Rectangle, len(keys))
|
||||
|
||||
for i, key := range keys {
|
||||
path, ok := resolve(key)
|
||||
if !ok {
|
||||
continue // 缺失纹理:跳过(占位)
|
||||
}
|
||||
tile, err := loadPNG(path)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("atlas.Build %s: %w", key, err)
|
||||
}
|
||||
x := (i % cols) * tileSize
|
||||
y := (i / cols) * tileSize
|
||||
r := image.Rect(x, y, x+tileSize, y+tileSize)
|
||||
// 最近邻缩放(非标准尺寸纹理保持像素风)
|
||||
scaled := scaleNearest(tile, tileSize)
|
||||
draw.Draw(img, r, scaled, image.Point{}, draw.Src)
|
||||
uv[key] = r
|
||||
}
|
||||
return img, uv, nil
|
||||
}
|
||||
|
||||
// loadPNG 解码 PNG。
|
||||
func loadPNG(path string) (image.Image, error) {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer f.Close()
|
||||
img, err := png.Decode(f)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return img, nil
|
||||
}
|
||||
|
||||
// scaleNearest 最近邻缩放到 tileSize×tileSize。
|
||||
func scaleNearest(src image.Image, size int) image.Image {
|
||||
sb := src.Bounds()
|
||||
if sb.Dx() == size && sb.Dy() == size {
|
||||
return src
|
||||
}
|
||||
dst := image.NewRGBA(image.Rect(0, 0, size, size))
|
||||
for y := 0; y < size; y++ {
|
||||
for x := 0; x < size; x++ {
|
||||
sx := sb.Min.X + x*sb.Dx()/size
|
||||
sy := sb.Min.Y + y*sb.Dy()/size
|
||||
dst.Set(x, y, src.At(sx, sy))
|
||||
}
|
||||
}
|
||||
return dst
|
||||
}
|
||||
59
internal/atlas/atlas_test.go
Normal file
59
internal/atlas/atlas_test.go
Normal file
@@ -0,0 +1,59 @@
|
||||
package atlas
|
||||
|
||||
import (
|
||||
"image"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// testResolve 从真实素材包解析纹理。
|
||||
func testResolve(key string) (string, bool) {
|
||||
p := filepath.Join("..", "..", "resourcepacks", "texture-pack-default1.20.5-26.2",
|
||||
"assets", "minecraft", "textures", key+".png")
|
||||
if _, err := filepath.Abs(p); err != nil {
|
||||
return "", false
|
||||
}
|
||||
if _, err := testStat(p); err != nil {
|
||||
return "", false
|
||||
}
|
||||
return p, true
|
||||
}
|
||||
|
||||
func testStat(p string) (interface{}, error) {
|
||||
f, err := filepath.Glob(p)
|
||||
if err != nil || len(f) == 0 {
|
||||
return nil, err
|
||||
}
|
||||
return f[0], nil
|
||||
}
|
||||
|
||||
// TestBuild 验证图集打包:UV 矩形齐全、图像尺寸正确。
|
||||
func TestBuild(t *testing.T) {
|
||||
keys := []string{"block/stone", "block/dirt", "block/grass_block_top"}
|
||||
img, uv, err := Build(testResolve, keys, 16)
|
||||
if err != nil {
|
||||
t.Fatalf("构建图集失败: %v", err)
|
||||
}
|
||||
b := img.Bounds()
|
||||
if b.Dx() != 32 || b.Dy() != 32 {
|
||||
t.Fatalf("图集尺寸期望 32×32,实际 %dx%d", b.Dx(), b.Dy())
|
||||
}
|
||||
for _, k := range keys {
|
||||
if _, ok := uv[k]; !ok {
|
||||
t.Fatalf("纹理 %s 缺 UV", k)
|
||||
}
|
||||
}
|
||||
// 三个键的 UV 不重叠且各不相同
|
||||
if uv[keys[0]] == uv[keys[1]] {
|
||||
t.Fatal("UV 矩形重叠")
|
||||
}
|
||||
}
|
||||
|
||||
// TestScaleNearest 验证最近邻缩放尺寸。
|
||||
func TestScaleNearest(t *testing.T) {
|
||||
src := image.NewRGBA(image.Rect(0, 0, 32, 32))
|
||||
dst := scaleNearest(src, 16)
|
||||
if db := dst.Bounds(); db.Dx() != 16 || db.Dy() != 16 {
|
||||
t.Fatalf("缩放后尺寸期望 16×16,实际 %dx%d", db.Dx(), db.Dy())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user