Files
aego/app/edit/art_shadow_workflow_gl_test.go
sanspie 2f613bc927
ci-platforms / darwin amd64 smoke (push) Skipped
ci-platforms / windows amd64 smoke (push) Skipped
ci-platforms / darwin arm64 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 / build (amd64, linux, ubuntu-24.04) (push) Successful in 20m43s
GitHub / windows amd64 native GitHub-hosted native CI: success
GitHub / darwin arm64 native GitHub-hosted native CI: failure
GitHub / darwin amd64 native GitHub-hosted native CI: success
GitHub / linux amd64 native GitHub-hosted native CI: success
ci / checks (push) Failing after 54m14s
ci / release (push) Skipped
fix: keep art tools accessible in maximized canvas
2026-11-10 09:16:25 +03:00

267 lines
8.5 KiB
Go

//go:build aego_gltest && linux && cgo && !aego_nonative
package edit
import (
"bytes"
"context"
"image"
"image/png"
"os"
"path/filepath"
"runtime"
"slices"
"testing"
"time"
"aego/art"
"aego/asset"
"aego/asset/importers/artsource"
"aego/core/ids"
"aego/core/log"
"aego/core/mathx"
"aego/editor/core/artedit"
"aego/editor/core/workspace"
"aego/modules/render2d"
"aego/render/lighting"
"aego/scene"
"aego/scene/frame"
)
// A transparent-tinted caster is invisible to the camera but still casts its
// alpha shadow. Pixel differences therefore come from the Lit receiver/shadow,
// not from merely repainting the caster sprite over the background.
func TestArtWorkingAlphaShadowNative(t *testing.T) {
runtime.LockOSThread()
defer runtime.UnlockOSThread()
p := newProject(t)
drawing, err := art.New(16, 16, art.RGBA, art.DefaultLimits())
if err != nil {
t.Fatal(err)
}
info := drawing.Info()
first, layer := info.Frames[0].ID, info.Layers[0].ID
e, err := drawing.Begin(drawing.Revision(), "Create two shadow frames")
if err != nil {
t.Fatal(err)
}
firstCel, err := e.EnsureCel(layer, first, art.Albedo)
if err != nil {
t.Fatal(err)
}
for y := 4; y < 12; y++ {
for x := 4; x < 12; x++ {
if err := e.SetPixel(firstCel, x, y, art.Paint{Color: art.Color{R: 255, G: 255, B: 255, A: 255}}, nil, false); err != nil {
t.Fatal(err)
}
}
}
second, err := e.AddFrame(first, 100)
if err != nil {
t.Fatal(err)
}
secondCel, err := e.EnsureCel(layer, second, art.Albedo)
if err != nil {
t.Fatal(err)
}
for y := 0; y < 4; y++ {
for x := 0; x < 4; x++ {
if err := e.SetPixel(secondCel, x, y, art.Paint{Color: art.Color{R: 255, G: 255, B: 255, A: 255}}, 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 := drawing.Encode()
drawing.Close()
if err != nil {
t.Fatal(err)
}
const artPath = "assets/shadow.aeart"
if err := os.WriteFile(filepath.Join(p.Root, artPath), data, 0644); err != nil {
t.Fatal(err)
}
imp := artsource.New()
meta := asset.NewSourceMeta(imp.ID(), imp.Version(), imp.Defaults())
root := ids.NewAssetID()
meta.ID = root
if err := os.WriteFile(filepath.Join(p.Root, artPath+".meta"), asset.MarshalSourceMeta(meta), 0644); err != nil {
t.Fatal(err)
}
const scenePath = "scenes/art-shadow.aescene"
writeScene(t, p, scenePath, func(sc *scene.Scene) {
camera := sc.Create(sc.Root(), "Camera")
*scene.Add[render2d.Camera2D](sc, camera) = render2d.DefaultCamera2D()
receiver := sc.Create(sc.Root(), "Lit receiver")
sr := scene.Add[render2d.SpriteRenderer](sc, receiver)
*sr = render2d.DefaultSpriteRenderer()
sr.Sprite = asset.NewRef[asset.Texture](asset.BuiltinWhite)
sr.Material = asset.NewRef[asset.Material](asset.BuiltinMaterialLit)
sr.Size = mathx.V2(14, 14)
sr.Order = -1
caster := sc.Create(sc.Root(), "Invisible alpha caster")
sp := scene.Add[render2d.SpriteRenderer](sc, caster)
*sp = render2d.DefaultSpriteRenderer()
sp.Sprite.ID, sp.Size, sp.Tint = root, mathx.V2(2, 2), mathx.Color{R: 1, G: 1, B: 1, A: 0}
oc := scene.Add[render2d.LightOccluder2D](sc, caster)
*oc = render2d.DefaultLightOccluder2D()
oc.Source = render2d.OccluderSpriteAlpha
lamp := sc.Create(sc.Root(), "Point light")
light := scene.Add[render2d.Light2D](sc, lamp)
*light = render2d.DefaultLight2D()
light.Range, light.Intensity, light.Shadows = 9, 2, true
pose := sc.Local(lamp)
pose.Position.X = -3
sc.SetLocal(lamp, pose)
env := scene.Add[render2d.LightingEnvironment2D](sc, sc.Create(sc.Root(), "Environment"))
*env = render2d.DefaultLightingEnvironment2D()
env.AmbientIntensity, env.MaxShadowedLights = .12, 4
})
s, err := Open(p, Config{Width: 1000, Height: 760, Hidden: true, NoWatch: true, Logger: log.Nop()})
if err != nil {
t.Fatal(err)
}
defer s.Close()
if !s.Step() {
t.Fatal("editor startup")
}
ed, ws := s.App.Context(), s.App.Workspace()
sceneDoc, err := ed.Docs.Open(scenePath)
if err != nil {
t.Fatal(err)
}
sceneDoc.View.Camera.Position = mathx.Vec2{}
panel := s.App.SceneView()
panel.SetZoom(40)
panel.SetLightingView(true, lighting.DebugShadows)
ed.Settings.Grid = false
ws.Show(workspace.Scene)
ws.Focus(workspace.Scene)
if ws.Tree.Maximized() != workspace.Scene && !ws.ToggleMaximize(workspace.Scene) {
t.Fatal("cannot show Scene View")
}
for range 5 {
if !s.Step() {
t.Fatal("scene setup")
}
}
artDoc, err := ed.OpenAsset(root)
if err != nil {
t.Fatal(err)
}
model, ok := artDoc.Binding.Model.(*artedit.Document)
if !ok {
t.Fatal("missing art document")
}
firstID := artsource.StableID(root, artsource.FrameName(first, art.Albedo))
secondID := artsource.StableID(root, artsource.FrameName(second, art.Albedo))
wait := func(previous uint64) {
t.Helper()
deadline := time.Now().Add(15 * time.Second)
for artDoc.WorkingPreview.Applied == previous && artDoc.WorkingPreview.Error == "" && time.Now().Before(deadline) {
if !s.Step() {
t.Fatal("preview stopped")
}
runtime.Gosched()
time.Sleep(time.Millisecond)
}
if artDoc.WorkingPreview.Applied == previous || artDoc.WorkingPreview.Error != "" || artDoc.WorkingPreview.Pending {
t.Fatalf("preview: %+v", artDoc.WorkingPreview)
}
}
read := func(name string) ([]byte, []mathx.Vec2) {
t.Helper()
ws.Focus(workspace.Scene)
// Opening an Art asset switches workspaces and restores the previously
// maximized Scene View. Compare the same actual view and target size on
// every capture rather than an old target against the new docked view.
if ws.Tree.Maximized() != workspace.Scene && !ws.ToggleMaximize(workspace.Scene) {
t.Fatal("cannot maximize Scene View for shadow capture")
}
for range 4 {
if !s.Step() {
t.Fatal("scene stopped")
}
}
raw, ok := panel.LightingWorld().ServiceAny(frame.ServiceID)
if !ok {
t.Fatal("Scene View runner missing")
}
runner := raw.(*frame.Runner)
if len(runner.Views()) != 1 {
t.Fatal("expected one Scene View")
}
input := runner.Bucket(0).Lighting
if len(input.Lights) != 1 || input.Disabled || input.Debug != lighting.DebugShadows {
t.Fatalf("scene lighting not active: %+v", input)
}
target, w, h := runner.Target()
t.Logf("shadow capture %s: target=%dx%d", name, w, h)
pixels := make([]byte, w*h*4)
if target.IsZero() || ed.Renderer.Device().ReadPixels(target, 0, 0, w, h, pixels) != nil {
t.Fatal("cannot read rendered Scene View")
}
if dir := os.Getenv("AEGO_ART_SHADOW_SHOTS"); dir != "" {
if err := os.MkdirAll(dir, 0755); err != nil {
t.Fatal(err)
}
f, err := os.Create(filepath.Join(dir, name+".png"))
if err != nil {
t.Fatal(err)
}
im := &image.RGBA{Pix: pixels, Stride: w * 4, Rect: image.Rect(0, 0, w, h)}
if err := png.Encode(f, im); err != nil {
f.Close()
t.Fatal(err)
}
if err := f.Close(); err != nil {
t.Fatal(err)
}
}
return pixels, slices.Clone(input.Points)
}
beforeShape, ok := ed.Loader.SpriteShadow(root)
if !ok || len(beforeShape.Parts) == 0 {
t.Fatal("initial first-frame shadow missing")
}
secondShape, ok := ed.Loader.SpriteShadow(secondID)
if !ok || len(secondShape.Parts) == 0 {
t.Fatal("second-frame alpha must exist")
}
before, geometry := read("before")
if len(geometry) == 0 {
t.Fatal("renderer extracted no initial occluder")
}
previous := artDoc.WorkingPreview.Applied
if _, err := model.Execute(context.Background(), art.Operation{Kind: "clear"}); err != nil {
t.Fatal(err)
}
wait(previous)
rootShape, rootOK := ed.Loader.SpriteShadow(root)
firstShape, firstOK := ed.Loader.SpriteShadow(firstID)
secondShape, secondOK := ed.Loader.SpriteShadow(secondID)
if !rootOK || !firstOK || !secondOK || len(rootShape.Parts) != 0 || len(firstShape.Parts) != 0 || len(secondShape.Parts) == 0 {
t.Fatalf("wrong working alpha: root=%+v first=%+v second=%+v", rootShape, firstShape, secondShape)
}
after, afterGeometry := read("after")
if len(afterGeometry) != 0 || bytes.Equal(before, after) {
t.Fatal("actual Scene View shadow did not change when only first-frame alpha was cleared")
}
previous = artDoc.WorkingPreview.Applied
if err := ed.UndoAsset(artDoc); err != nil {
t.Fatal(err)
}
wait(previous)
restored, restoredGeometry := read("undo")
if !slices.Equal(geometry, restoredGeometry) || !bytes.Equal(before, restored) {
t.Fatal("Undo did not restore rendered alpha shadow")
}
t.Logf("native shadow: initial=%d vertices, cleared=%d, undo=%d; GPU pixels changed and restored", len(geometry), len(afterGeometry), len(restoredGeometry))
}