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

259 lines
9.6 KiB
Go

package editor
import (
"aego/core/ids"
"aego/core/mathx"
"aego/editor/core/context"
"aego/engine/i18n"
"aego/gpu"
"aego/render"
"aego/render/lighting"
"aego/render/material"
"aego/render/mesh"
"aego/render/postfx"
"aego/render/rt"
"aego/render/shader"
"aego/render/sprite"
"aego/render/texture"
"aego/render/view"
"aego/scene/frame"
"aego/ui/widgets"
"fmt"
)
type materialPreview struct {
mat *material.Material
stamp, revision uint64
pool *rt.Pool
target *rt.Target
quad *mesh.Mesh
reason string
renderError string
viewID view.ID
lighting lighting.Input
lights [2]lighting.Light
customPlan postfx.Plan
}
func (p *materialPreview) lightingInput() *lighting.Input {
if p.viewID.Scope == 0 {
p.viewID = frame.New().OutputID()
}
p.lighting.Environment = lighting.DefaultEnvironment()
p.lighting.Environment.AmbientIntensity = .18
p.lighting.Environment.ColorMode = lighting.LDR
p.lighting.Environment.MaxShadowedLights = 0
p.lighting.HasEnvironment = true
p.lights = [2]lighting.Light{
{ID: ids.NodeID{Lo: 1}, Type: lighting.Point, Position: mathx.Vec2{X: -2, Y: -1}, Direction: mathx.Vec2{X: 1}, Color: mathx.Color{R: 1, G: .8, B: .65, A: 1}, Intensity: 1.5, Range: 6, Height: 2, Falloff: 1, Layers: ^uint64(0)},
{ID: ids.NodeID{Lo: 2}, Type: lighting.Directional, Direction: mathx.Vec2{X: -1}, Color: mathx.Color{R: .5, G: .65, B: 1, A: 1}, Intensity: .35, Elevation: .7, Layers: ^uint64(0)},
}
p.lighting.Lights = p.lights[:]
return &p.lighting
}
func (p *materialPreview) update(ed *context.Editor, m *material.Material, stamp, revision uint64) {
if p.mat != nil && p.stamp == stamp && p.revision == revision {
return
}
p.stamp, p.revision = stamp, revision
if ed.Renderer == nil || ed.Renderer.Caps().Renderer == "null" {
p.reason = i18n.Tr("editor.material.preview_no_gpu", "Preview unavailable: this session has no graphics device.")
return
}
p.reason = ""
cache := ed.Renderer.Materials()
if p.mat == nil {
p.mat = m
cache.Track(m)
} else {
cache.QueueMaterial(p.mat, m, nil)
}
}
func (p *materialPreview) drawLast(ed *context.Editor) {
if p.mat == nil {
widgets.LabelWrapped(ed.UI, i18n.Tr("editor.material.preview_none", "No successfully compiled preview is available."), false)
return
}
widgets.LabelWrapped(ed.UI, i18n.Tr("editor.material.preview_last_good", "Preview keeps its previous definition while the current source is invalid."), false)
p.draw(ed)
}
func (p *materialPreview) draw(ed *context.Editor) {
ui := ed.UI
if p.reason != "" {
widgets.LabelWrapped(ui, p.reason, true)
return
}
if p.mat == nil || ed.Renderer == nil {
return
}
if p.viewID.Scope == 0 {
p.viewID = frame.New().OutputID()
}
if p.renderError != "" {
widgets.LabelWrapped(ui, p.renderError, true)
}
cache := ed.Renderer.Materials()
if cache.Pending(p.mat) {
widgets.LabelWrapped(ui, i18n.Tr("editor.material.preview_pending", "Preview: separate session, compiling candidate; generation {generation} remains visible.", i18n.Int("generation", int64(p.mat.Generation()))), false)
} else if p.mat.Failed() {
widgets.LabelWrapped(ui, i18n.Tr("editor.material.preview_rejected", "Preview: candidate rejected; previous generation {generation} is retained. See Problems.", i18n.Int("generation", int64(p.mat.Generation()))), true)
} else {
widgets.LabelWrapped(ui, i18n.Tr("editor.material.preview_committed", "Preview: separate material session, generation {generation}.", i18n.Int("generation", int64(p.mat.Generation()))), false)
}
const size = 192
if err := p.prepare(ed, size); err != nil {
widgets.LabelWrapped(ui, err.Error(), true)
return
}
target, mat := p.target, p.mat
ed.OnRender(func() {
if p.target == target && p.mat == mat {
p.renderSurface(ed, size)
}
})
area := ui.Cursor().Next(size)
area.W = min(area.W, size)
// Standard/PMA previews remain transparent over checkerboard. Additive
// previews own an opaque backdrop so zero-coverage energy remains visible.
cell := float32(12)
for y := float32(0); y < area.H; y += cell {
for x := float32(0); x < area.W; x += cell {
shade := float32(.12)
if (int(x/cell)+int(y/cell))%2 != 0 {
shade = .3
}
ui.Rect(mathx.Rect{X: area.X + x, Y: area.Y + y, W: min(cell, area.W-x), H: min(cell, area.H-y)}, mathx.Color{R: shade, G: shade, B: shade, A: 1})
}
}
ui.List().Texture(target.Region(), area, mathx.Color{R: 1, G: 1, B: 1, A: 1})
if !ed.Renderer.Materials().Ready(mat) {
widgets.LabelWrapped(ui, i18n.Tr("editor.material.preview_failed", "GPU compilation failed; see Problems for the compiler log."), true)
}
}
func (p *materialPreview) close(ed *context.Editor) {
if ed.Renderer != nil {
if p.viewID.Scope != 0 {
_ = ed.Renderer.ReleaseLightingView(p.viewID, p.viewID)
p.viewID = view.ID{}
}
if p.mat != nil {
ed.Renderer.Materials().Release(p.mat)
ed.Renderer.Materials().Retire(p.mat.Shader())
p.mat = nil
}
if p.quad != nil {
p.quad.Destroy(ed.Renderer.Device())
p.quad = nil
}
}
if p.pool != nil {
p.pool.Release(p.target)
p.target = nil
p.pool.Close()
p.pool = nil
}
}
func (p *materialPreview) prepare(ed *context.Editor, size int) error {
if p.pool == nil {
p.pool = rt.New(ed.Renderer.Device(), gpu.TexSRGBA8, "material.preview")
}
if p.target == nil {
target, err := p.pool.Acquire(size, size, true)
if err != nil {
p.reason = err.Error()
return err
}
p.target = target
}
if p.mat.Shader().Domain == shader.DomainMesh2D && p.quad == nil {
data := mesh.Data{Vertices: []mesh.Vertex{
{Pos: mathx.Vec3{X: -1, Y: -1}, Normal: mathx.Vec3{Z: 1}, UV: mathx.Vec2{}, Color: mesh.White},
{Pos: mathx.Vec3{X: 1, Y: -1}, Normal: mathx.Vec3{Z: 1}, UV: mathx.Vec2{X: 1}, Color: mesh.White},
{Pos: mathx.Vec3{X: 1, Y: 1}, Normal: mathx.Vec3{Z: 1}, UV: mathx.Vec2{X: 1, Y: 1}, Color: mesh.White},
{Pos: mathx.Vec3{X: -1, Y: 1}, Normal: mathx.Vec3{Z: 1}, UV: mathx.Vec2{Y: 1}, Color: mesh.White},
}, Indices: []uint32{0, 1, 2, 0, 2, 3}}
data.ComputeBounds()
quad, err := mesh.Upload(ed.Renderer.Device(), &data, "material.preview.quad")
if err != nil {
p.reason = err.Error()
return err
}
p.quad = quad
}
return nil
}
func (p *materialPreview) renderSurface(ed *context.Editor, size int) {
if p.viewID.Scope == 0 {
p.viewID = frame.New().OutputID()
}
target, mat, quad := p.target, p.mat, p.quad
r := ed.Renderer
background := mathx.Color{}
if mat.Blend() == shader.BlendAdditive {
// Additive energy preserves existing coverage. Preview it against an
// opaque backdrop, not a falsely compositable alpha-zero image.
background = mathx.Color{R: .12, G: .12, B: .12, A: 1}
}
p.renderError = ""
if mat.Shader().Domain == shader.DomainPostFX {
if len(p.customPlan.Passes) == 0 {
d, _ := postfx.NewRegistry().Lookup(postfx.CustomID)
p.customPlan.Passes = []postfx.Pass{{Descriptor: d}}
}
p.customPlan.Passes[0].Material = mat
if err := drawPostScene(r, p.viewID, target, size, &p.customPlan, "material.postfx-preview"); err != nil {
p.renderError = err.Error()
}
return
}
f := r.View(view.View{ID: p.viewID, OutputID: p.viewID, Target: target.Texture, DepthTarget: target.Depth, TargetW: target.BucketW, TargetH: target.BucketH, Viewport: mathx.Rect{W: float32(size), H: float32(size)}, PixelScale: 1, Camera: view.Camera2D{Zoom: float32(size) / 3}, DoClear: true, Clear: background, Label: i18n.Tr("editor.material.preview_name", "Material preview")})
if mat.Shader().Features&shader.FeatureLighting != 0 {
if err := f.Lighting(p.lightingInput()); err != nil {
p.renderError = err.Error()
}
}
normal, _, _ := mat.Normal()
white := mathx.Color{R: 1, G: 1, B: 1, A: 1}
switch mat.Shader().Domain {
case shader.DomainMesh2D:
f.Mesh(mesh.Draw{VisibilityLayers: ^uint64(0), Normal: normal, Mesh: quad, World: mathx.Mat4Identity(), Texture: r.White(), Color: white, Material: mat, DoubleSided: true, Is2D: true})
case shader.DomainSprite:
f.Sprite(sprite.Draw{VisibilityLayers: ^uint64(0), Normal: normal, Region: r.White(), Size: mathx.Vec2{X: 2, Y: 2}, Origin: mathx.Vec2{X: .5, Y: .5}, Tint: white, Material: mat})
}
f.End()
}
// Render into a private linear camera target, then bind its colour as MainTex to resolve the fullscreen material into the preview; no sprite draw owns that material.
func drawPostScene(r *render.Renderer, id view.ID, target *rt.Target, size int, plan *postfx.Plan, label string) error {
prev := r.Stats()
f := r.View(view.View{ID: id, OutputID: id, Target: target.Texture, TargetW: target.BucketW, TargetH: target.BucketH, Viewport: mathx.Rect{W: float32(size), H: float32(size)}, Camera: view.Camera2D{Zoom: float32(size) / 3}, Layers: view.AllLayers, PixelsPerUnit: 1, PixelScale: 1, DoClear: true, Clear: mathx.Color{A: 1}, Label: label})
f.PostProcess(plan)
drawPostSample(f, r.White())
f.End()
next := r.Stats()
if next.PostFXFailures != prev.PostFXFailures {
return r.PostProcessError()
}
if len(plan.Passes) > 0 && next.PostFXPasses == prev.PostFXPasses {
return fmt.Errorf("PostFX preview did not execute its full-screen pass")
}
return nil
}
// The same high-contrast scene makes channel, vignette and spatial effects
// visible on both the material document and the Assets tile.
func drawPostSample(f *render.ViewFrame, white texture.Region) {
for j := range 4 {
for i := range 4 {
tint := mathx.Color{R: float32(i+1) / 4, G: float32(j+1) / 4, B: float32((i + j) % 2), A: 1}
f.Sprite(sprite.Draw{VisibilityLayers: ^uint64(0), Region: white, Size: mathx.Vec2{X: .75, Y: .75}, Position: mathx.Vec2{X: (float32(i) - 1.5) * .75, Y: (float32(j) - 1.5) * .75}, Origin: mathx.Vec2{X: .5, Y: .5}, Tint: tint})
}
}
}