Files
sanspie a6822156cd
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
Finish Stage 12 editor fixes and refreshed platformer
2026-11-05 17:36:42 +03:00

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