Files
aego/asset/importers/artsource/surface_channels_test.go
sanspie a46347e2c6
ci-platforms / darwin arm64 smoke (push) Skipped
ci / build (amd64, linux, ubuntu-24.04) (push) Failing after 12m19s
ci-platforms / darwin amd64 smoke (push) Skipped
ci-platforms / windows amd64 smoke (push) Skipped
github-native / gitea-pending (push) Skipped
github-native / darwin amd64 native (push) Skipped
github-native / linux amd64 native (push) Skipped
github-native / windows amd64 native (push) Skipped
github-native / darwin arm64 native (push) Skipped
github-native / gitea-result (push) Skipped
ci / checks (push) Failing after 23m57s
ci / release (push) Skipped
GitHub / windows amd64 native GitHub-hosted native CI: failure
GitHub / darwin arm64 native GitHub-hosted native CI: failure
GitHub / darwin amd64 native GitHub-hosted native CI: failure
GitHub / linux amd64 native GitHub-hosted native CI: success
editor: add art studio and stage 12.5 workflows
2026-11-08 18:47:57 +03:00

250 lines
7.3 KiB
Go

package artsource
import (
"bytes"
"image"
"image/color"
"image/png"
"os"
"path/filepath"
"testing"
"aego/art"
"aego/asset"
teximport "aego/asset/importers/texture"
"aego/asset/pipeline"
"aego/core/ids"
)
// Comparing the prepared artifacts, rather than only the edit model, catches
// changes in channel semantics and hidden RGB used by the real runner shader.
func TestRunnerSurfaceArtifactsKeepFramePixels(t *testing.T) {
root := filepath.Join("..", "..", "..", "examples", "platformer", "assets", "surfaces")
read := func(name string) []byte {
t.Helper()
b, err := os.ReadFile(filepath.Join(root, name))
if err != nil {
t.Fatal(err)
}
return b
}
colorPNG, normalPNG := read("runner.png"), read("runner_n.png")
colorMeta, err := asset.ParseSourceMeta(read("runner.png.meta"), "runner.png.meta")
if err != nil {
t.Fatal(err)
}
normalMeta, err := asset.ParseSourceMeta(read("runner_n.png.meta"), "runner_n.png.meta")
if err != nil {
t.Fatal(err)
}
if len(colorMeta.Subs) != 8 || len(normalMeta.Subs) != 8 {
t.Fatal("unexpected canonical runner slices")
}
albedoImage, err := png.Decode(bytes.NewReader(colorPNG))
if err != nil {
t.Fatal(err)
}
normalImage, err := png.Decode(bytes.NewReader(normalPNG))
if err != nil {
t.Fatal(err)
}
d, err := art.New(16, 16, art.RGBA, art.Limits{})
if err != nil {
t.Fatal(err)
}
defer d.Close()
info := d.Info()
e, err := d.Begin(d.Revision(), "Reproduce runner surface frames")
if err != nil {
t.Fatal(err)
}
frame, layer := info.Frames[0].ID, info.Layers[0].ID
for i, sub := range colorMeta.Subs {
if i > 0 {
frame, err = e.AddFrame(frame, 100)
if err != nil {
t.Fatal(err)
}
}
r := sub.Rect
if r[2] != 16 || r[3] != 16 {
t.Fatalf("unexpected runner frame %d: %v", i, r)
}
for _, source := range []struct {
ch art.Channel
img image.Image
}{{art.Albedo, albedoImage}, {art.Normal, normalImage}} {
cel, err := e.EnsureCel(layer, frame, source.ch)
if err != nil {
t.Fatal(err)
}
for y := 0; y < 16; y++ {
for x := 0; x < 16; x++ {
v := color.NRGBAModel.Convert(source.img.At(r[0]+x, r[1]+y)).(color.NRGBA)
if err := e.SetPixel(cel, x, y, art.Paint{Color: art.Color{R: v.R, G: v.G, B: v.B, A: v.A}}, nil, false); err != nil {
t.Fatal(err)
}
}
}
}
}
change, err := e.Prepare()
if err != nil {
t.Fatal(err)
}
if err := change.Apply(); err != nil {
t.Fatal(err)
}
src, err := d.Encode()
if err != nil {
t.Fatal(err)
}
newMeta := asset.NewSourceMeta("artsource", Version, asset.TextureDefaults())
for i, frame := range d.Info().Frames {
newMeta.Subs = append(newMeta.Subs,
asset.SourceFrame{Name: FrameName(frame.ID, art.Albedo), ID: colorMeta.Subs[i].ID},
asset.SourceFrame{Name: FrameName(frame.ID, art.Normal), ID: normalMeta.Subs[i].ID})
}
oldColor, err := teximport.New().Import(&pipeline.ImportContext{ID: colorMeta.ID, Path: "assets/surfaces/runner.png", Source: colorPNG, Meta: colorMeta})
if err != nil {
t.Fatal(err)
}
oldNormal, err := teximport.New().Import(&pipeline.ImportContext{ID: normalMeta.ID, Path: "assets/surfaces/runner_n.png", Source: normalPNG, Meta: normalMeta})
if err != nil {
t.Fatal(err)
}
converted, err := New().Import(&pipeline.ImportContext{ID: newMeta.ID, Path: "assets/surfaces/runner.aeart", Source: src, Meta: newMeta})
if err != nil {
t.Fatal(err)
}
getTex := func(list []pipeline.Produced) *asset.TextureArtifact {
t.Helper()
for _, a := range list {
if a.Kind == asset.ArtTexture {
tex, err := asset.DecodeTexture(a.Data)
if err != nil {
t.Fatal(err)
}
return tex
}
}
t.Fatal("missing texture artifact")
return nil
}
getRegion := func(result *pipeline.Result, id ids.AssetID) *asset.RegionArtifact {
t.Helper()
for _, sub := range result.Subs {
if sub.ID == id {
for _, a := range sub.Artifacts {
if a.Kind == asset.ArtSpriteRegion {
v, err := asset.DecodeRegion(a.Data)
if err != nil {
t.Fatal(err)
}
return v
}
}
}
}
t.Fatalf("missing region %s", id)
return nil
}
newAlbedo := getTex(converted.Artifacts)
var newNormal *asset.TextureArtifact
for _, sub := range converted.Subs {
if sub.Name == "surface_normal" {
newNormal = getTex(sub.Artifacts)
}
}
if newNormal == nil {
t.Fatal("normal sheet missing")
}
oldAlbedoTex, oldNormalTex := getTex(oldColor.Artifacts), getTex(oldNormal.Artifacts)
compare := func(label string, a, b *asset.TextureArtifact, ar, br *asset.RegionArtifact) {
t.Helper()
if a.Format != b.Format || a.Semantic != b.Semantic || ar.W != br.W || ar.H != br.H || ar.Filter != br.Filter || ar.HasPivot != br.HasPivot || ar.Pivot != br.Pivot {
t.Fatalf("%s texture or region metadata changed", label)
}
for y := 0; y < ar.H; y++ {
x := ((ar.Y+y)*a.Width + ar.X) * 4
z := ((br.Y+y)*b.Width + br.X) * 4
if !bytes.Equal(a.Pixels[x:x+ar.W*4], b.Pixels[z:z+br.W*4]) {
t.Fatalf("%s prepared pixels differ in row %d", label, y)
}
}
}
for i, sub := range colorMeta.Subs {
before, after := getRegion(oldColor, sub.ID), getRegion(converted, sub.ID)
if before.Normal != normalMeta.Subs[i].ID || after.Normal != before.Normal {
t.Fatalf("normal AssetID changed: before=%s after=%s", before.Normal, after.Normal)
}
compare("albedo", oldAlbedoTex, newAlbedo, before, after)
compare("normal", oldNormalTex, newNormal, getRegion(oldNormal, normalMeta.Subs[i].ID), getRegion(converted, normalMeta.Subs[i].ID))
}
}
func TestSurfaceArtifactRetainsDataRGBAtZeroAlpha(t *testing.T) {
d, err := art.New(16, 16, art.RGBA, art.Limits{})
if err != nil {
t.Fatal(err)
}
defer d.Close()
info := d.Info()
e, err := d.Begin(d.Revision(), "surface pixels")
if err != nil {
t.Fatal(err)
}
for _, spec := range []struct {
channel art.Channel
value art.Color
}{
{art.Normal, art.Color{R: 128, G: 129, B: 255}}, {art.Height, art.Color{R: 91, G: 91, B: 91}}, {art.Emission, art.Color{R: 33, G: 144, B: 233}},
} {
cel, err := e.EnsureCel(info.Layers[0].ID, info.Frames[0].ID, spec.channel)
if err != nil {
t.Fatal(err)
}
if err := e.SetPixel(cel, 0, 0, art.Paint{Color: spec.value}, nil, false); err != nil {
t.Fatal(err)
}
}
change, err := e.Prepare()
if err != nil {
t.Fatal(err)
}
if err := change.Apply(); err != nil {
t.Fatal(err)
}
data, err := d.Encode()
if err != nil {
t.Fatal(err)
}
meta := asset.NewSourceMeta("artsource", Version, asset.TextureDefaults())
result, err := New().Import(&pipeline.ImportContext{ID: meta.ID, Path: "assets/surface.aeart", Source: data, Meta: meta})
if err != nil {
t.Fatal(err)
}
// The emission importer deliberately makes its alpha opaque; RGB is data.
want := map[string][]byte{"surface_normal": {128, 129, 255, 0}, "surface_height": {91, 91, 91, 0}, "surface_emission": {33, 144, 233, 255}}
for _, sub := range result.Subs {
v, ok := want[sub.Name]
if !ok {
continue
}
if len(sub.Artifacts) != 1 || sub.Artifacts[0].Kind != asset.ArtTexture {
t.Fatalf("%s has no texture artifact", sub.Name)
}
tex, err := asset.DecodeTexture(sub.Artifacts[0].Data)
if err != nil {
t.Fatal(err)
}
if len(tex.Pixels) < 4 || !bytes.Equal(tex.Pixels[:4], v) {
t.Fatalf("%s lost RGB: %v want %v", sub.Name, tex.Pixels, v)
}
delete(want, sub.Name)
}
if len(want) != 0 {
t.Fatalf("surface artifacts missing: %v", want)
}
}