ci-platforms / windows amd64 smoke (push) Skipped
ci / release (push) Successful in 8m58s
ci-platforms / darwin amd64 smoke (push) Skipped
ci-platforms / darwin arm64 smoke (push) Skipped
ci / checks (push) Successful in 4m46s
ci / build (amd64, linux, ubuntu-24.04) (push) Successful in 13m28s
232 lines
7.7 KiB
Go
232 lines
7.7 KiB
Go
// Command platformer-surfaces regenerates deterministic platformer surface assets from the repository root, preserving existing .meta settings.
|
|
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/sha256"
|
|
"encoding/binary"
|
|
"fmt"
|
|
"image"
|
|
"image/color"
|
|
"image/png"
|
|
"math"
|
|
"os"
|
|
"path/filepath"
|
|
|
|
"aego/asset"
|
|
teximport "aego/asset/importers/texture"
|
|
"aego/asset/pipeline"
|
|
"aego/core/ids"
|
|
"aego/format/aetext"
|
|
)
|
|
|
|
const output = "examples/platformer/assets/surfaces"
|
|
|
|
func id(name string) ids.AssetID {
|
|
h := sha256.Sum256([]byte("aego.platformer.12b/" + name))
|
|
return ids.AssetID{Hi: binary.BigEndian.Uint64(h[:8]), Lo: binary.BigEndian.Uint64(h[8:16])}
|
|
}
|
|
|
|
func check(err error) {
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
}
|
|
|
|
func pngBytes(img *image.NRGBA) []byte {
|
|
var b bytes.Buffer
|
|
check(png.Encode(&b, img))
|
|
return b.Bytes()
|
|
}
|
|
|
|
func imageOf(w, h int, f func(int, int) color.NRGBA) *image.NRGBA {
|
|
img := image.NewNRGBA(image.Rect(0, 0, w, h))
|
|
for y := 0; y < h; y++ {
|
|
for x := 0; x < w; x++ {
|
|
img.SetNRGBA(x, y, f(x, y))
|
|
}
|
|
}
|
|
return img
|
|
}
|
|
|
|
func gray(v int, alpha byte) color.NRGBA {
|
|
c := byte(max(0, min(v, 255)))
|
|
return color.NRGBA{R: c, G: c, B: c, A: alpha}
|
|
}
|
|
|
|
func meta(name, semantic, group string, subs []pipeline.SubAsset, normal ids.AssetID, generate bool) *pipeline.Meta {
|
|
s := teximport.New().Defaults()
|
|
s["semantic"] = aetext.Ident(semantic)
|
|
s["atlas"] = aetext.String(group)
|
|
s["srgb"] = aetext.Bool(semantic == "color")
|
|
s["auto_normal"] = aetext.Bool(false)
|
|
s["generate_normal"] = aetext.Bool(generate)
|
|
s["height_strength"] = aetext.Float32(3)
|
|
if !normal.IsZero() {
|
|
s["normal"] = aetext.AssetRef(normal.String())
|
|
}
|
|
return &pipeline.Meta{ID: id(name), Importer: "texture", Version: teximport.Version, Settings: s, Subs: subs}
|
|
}
|
|
|
|
func write(name string, img *image.NRGBA, m *pipeline.Meta) {
|
|
file := filepath.Join(output, name+".png")
|
|
check(os.WriteFile(file, pngBytes(img), 0o644))
|
|
if _, err := os.Stat(file + ".meta"); os.IsNotExist(err) {
|
|
check(os.WriteFile(file+".meta", pipeline.WriteMeta(m), 0o644))
|
|
} else {
|
|
check(err)
|
|
data, err := os.ReadFile(file + ".meta")
|
|
check(err)
|
|
old, err := pipeline.ParseMeta(data, file+".meta")
|
|
check(err)
|
|
if old.ID != m.ID {
|
|
panic("surface metadata identity differs: " + name)
|
|
}
|
|
}
|
|
fmt.Printf("%s %s\n", name, m.ID)
|
|
}
|
|
|
|
func normals(height *image.NRGBA, subs []pipeline.SubAsset) *image.NRGBA {
|
|
m := meta("generated", "height", "", subs, ids.AssetID{}, true)
|
|
res, err := teximport.New().Import(&pipeline.ImportContext{ID: m.ID, Path: "height.png", Source: pngBytes(height), Meta: m})
|
|
check(err)
|
|
d, err := asset.DecodeTexture(res.Artifacts[0].Data)
|
|
check(err)
|
|
img := image.NewNRGBA(image.Rect(0, 0, d.Width, d.Height))
|
|
copy(img.Pix, d.Pixels)
|
|
return img
|
|
}
|
|
|
|
func main() {
|
|
check(os.MkdirAll(output, 0o755))
|
|
// Albedo stays neutral, preserving each existing scene/prefab tint. Only
|
|
// the shallow relief changes light response; collision geometry is intact.
|
|
ground := imageOf(128, 16, func(x, y int) color.NRGBA {
|
|
v := 231 + (x*17+y*11)%7
|
|
if y < 2 {
|
|
v = 250
|
|
}
|
|
if y == 15 || x%16 == 0 && y > 2 {
|
|
v = 193
|
|
}
|
|
return gray(v, 255)
|
|
})
|
|
groundHeight := imageOf(128, 16, func(x, y int) color.NRGBA {
|
|
edge := min(x%16, 15-x%16, y, 15-y)
|
|
return gray(96+min(edge, 3)*36, 255)
|
|
})
|
|
write("ground", ground, meta("ground", "color", "platformer", nil, id("ground_n"), false))
|
|
write("ground_height", groundHeight, meta("ground_height", "height", "", nil, ids.AssetID{}, false))
|
|
write("ground_n", normals(groundHeight, nil), meta("ground_n", "normal", "platformer", nil, ids.AssetID{}, false))
|
|
|
|
platform := imageOf(96, 16, func(x, y int) color.NRGBA {
|
|
v := 231
|
|
if y < 2 {
|
|
v = 252
|
|
}
|
|
if y > 12 {
|
|
v = 200
|
|
}
|
|
if (x%24 == 3 || x%24 == 20) && (y == 5 || y == 10) {
|
|
v = 180
|
|
}
|
|
return gray(v, 255)
|
|
})
|
|
platformHeight := imageOf(96, 16, func(x, y int) color.NRGBA {
|
|
v := 128 + min(y, 15-y, 4)*24
|
|
if (x%24 == 3 || x%24 == 20) && (y == 5 || y == 10) {
|
|
v = 80
|
|
}
|
|
return gray(v, 255)
|
|
})
|
|
platformMeta := meta("platform", "color", "platformer", nil, ids.AssetID{}, false)
|
|
platformMeta.Settings["height"] = aetext.AssetRef(id("platform_height").String())
|
|
write("platform", platform, platformMeta)
|
|
// This normal map is generated by the real importer, on import/reimport.
|
|
write("platform_height", platformHeight, meta("platform_height", "height", "platformer", nil, ids.AssetID{}, true))
|
|
|
|
wall := imageOf(64, 64, func(x, y int) color.NRGBA {
|
|
row := y / 16
|
|
xx := (x + (row%2)*8) % 16
|
|
v := 226 + (x*7+y*13)%5
|
|
if xx == 0 || y%16 == 0 {
|
|
v = 183
|
|
}
|
|
return gray(v, 255)
|
|
})
|
|
wallHeight := imageOf(64, 64, func(x, y int) color.NRGBA {
|
|
xx := (x + ((y/16)%2)*8) % 16
|
|
yy := y % 16
|
|
v := 105 + int(115*math.Sin(float64(xx)*math.Pi/16)*math.Sin(float64(yy)*math.Pi/16))
|
|
return gray(v, 255)
|
|
})
|
|
write("wall", wall, meta("wall", "color", "platformer", nil, id("wall_n"), false))
|
|
write("wall_height", wallHeight, meta("wall_height", "height", "", nil, ids.AssetID{}, false))
|
|
write("wall_n", normals(wallHeight, nil), meta("wall_n", "normal", "platformer", nil, ids.AssetID{}, false))
|
|
|
|
// The original eight runner silhouettes and eye pixels are copied exactly.
|
|
runner := image.NewNRGBA(image.Rect(0, 0, 128, 16))
|
|
height := image.NewNRGBA(image.Rect(0, 0, 128, 16))
|
|
var aSubs, nSubs []pipeline.SubAsset
|
|
var bindings []aetext.Value
|
|
for i := 0; i < 8; i++ {
|
|
src, ok := asset.BuiltinTexture(asset.BuiltinRunner(i))
|
|
if !ok {
|
|
panic("original runner frame missing")
|
|
}
|
|
name := fmt.Sprintf("runner_%03d", i)
|
|
a := pipeline.SubAsset{ID: id("runner/" + name), Name: name, Rect: [4]int{i * 16, 0, 16, 16}}
|
|
n := pipeline.SubAsset{ID: id("runner_n/" + name), Name: name, Rect: a.Rect}
|
|
aSubs, nSubs = append(aSubs, a), append(nSubs, n)
|
|
bindings = append(bindings, aetext.Struct(aetext.Field{Name: "albedo", Value: aetext.AssetRef(a.ID.String())}, aetext.Field{Name: "normal", Value: aetext.AssetRef(n.ID.String())}))
|
|
for y := 0; y < 16; y++ {
|
|
for x := 0; x < 16; x++ {
|
|
p := src.Pixels[(y*16+x)*4:][:4]
|
|
runner.SetNRGBA(i*16+x, y, color.NRGBA{R: p[0], G: p[1], B: p[2], A: p[3]})
|
|
v := 0
|
|
if p[3] != 0 {
|
|
v = 125 + int(95*math.Max(0, 1-math.Pow((float64(x)-7.5)/5, 2)))
|
|
}
|
|
height.SetNRGBA(i*16+x, y, gray(v, p[3]))
|
|
}
|
|
}
|
|
}
|
|
am := meta("runner", "color", "", aSubs, id("runner_n"), false)
|
|
am.Settings["normal_frames"] = aetext.List(bindings...)
|
|
write("runner", runner, am)
|
|
write("runner_height", height, meta("runner_height", "height", "", nil, ids.AssetID{}, false))
|
|
write("runner_n", normals(height, nSubs), meta("runner_n", "normal", "", nSubs, ids.AssetID{}, false))
|
|
|
|
lamp := imageOf(8, 16, func(x, y int) color.NRGBA {
|
|
if x < 2 || x > 5 || y < 3 || y > 13 {
|
|
return color.NRGBA{}
|
|
}
|
|
if y == 3 || y == 13 {
|
|
return gray(170, 255)
|
|
}
|
|
return gray(255, 255)
|
|
})
|
|
lampMeta := meta("lamp", "color", "platformer", nil, ids.AssetID{}, false)
|
|
lampMeta.Settings["emission"] = aetext.AssetRef(id("lamp_em").String())
|
|
write("lamp", lamp, lampMeta)
|
|
// Emission is a scalar light-core mask, independent of albedo coverage. It
|
|
// deliberately leaves the lamp caps dim and does not add a halo or Bloom.
|
|
lampMask := imageOf(8, 16, func(x, y int) color.NRGBA {
|
|
if x >= 2 && x <= 5 && y >= 4 && y <= 12 {
|
|
return gray(255, 255)
|
|
}
|
|
return gray(0, 255)
|
|
})
|
|
write("lamp_em", lampMask, meta("lamp_em", "emission-mask", "platformer", nil, ids.AssetID{}, false))
|
|
cookie := imageOf(64, 64, func(x, y int) color.NRGBA {
|
|
dx, dy := (float64(x)+.5)/32-1, (float64(y)+.5)/32-1
|
|
v := math.Max(0, 1-dx*dx-dy*dy)
|
|
// A broad window slat makes the cookie distinguishable from a Point.
|
|
if math.Abs(dx) < .09 {
|
|
v *= .3
|
|
}
|
|
return gray(int(v*v*255+.5), 255)
|
|
})
|
|
write("window_cookie", cookie, meta("window_cookie", "cookie-mask", "", nil, ids.AssetID{}, false))
|
|
}
|