Files
aego/render/lighting_preview.go
T
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

148 lines
5.0 KiB
Go

package render
import (
"encoding/binary"
"fmt"
"math"
"aego/core/mathx"
"aego/gpu"
"aego/render/view"
)
type shadowPreviewState struct {
shader gpu.ShaderHandle
pipe gpu.PipelineHandle
uniform gpu.BufferHandle
sampler gpu.SamplerHandle
data [16]byte
}
// PreviewShadowLayer draws a light-space visibility mask between views during BeginFrame/EndFrame. The existing RGBA8/SRGBA8 2D target
// must match width/height at its base level; invalid requests never draw to the window.
// It borrows the current source at execution time, never retains array handles across lighting frames, resizes or context generations, and changes no material, light or camera settings.
func (r *Renderer) PreviewShadowLayer(id view.ID, layer int, target gpu.TextureHandle, width, height int) error {
if r == nil || r.frame == nil {
return fmt.Errorf("shadow preview requires an active render frame")
}
if r.vf.pass != nil {
return fmt.Errorf("shadow preview must run between completed views")
}
if state, ok := r.dev.(gpu.ContextState); ok && state.ContextLost() {
return fmt.Errorf("shadow preview is unavailable while the graphics context is lost")
}
source, _, layers, ok := r.LightingResources(id)
if !ok {
return fmt.Errorf("this view has no prepared shadow masks")
}
if layer < 0 || layer >= layers {
return fmt.Errorf("shadow preview layer %d is outside [0,%d)", layer, layers)
}
if cached := &r.lightState.views[id].shadow[layer]; !cached.active || !cached.valid {
return fmt.Errorf("shadow preview layer %d is not active in the current view", layer)
}
describer, ok := r.dev.(gpu.TextureDescriber)
if !ok {
return fmt.Errorf("graphics device cannot validate a shadow preview target")
}
desc, ok := describer.TextureInfo(target)
if !ok || desc.Dimension != gpu.Texture2D || desc.Width != width || desc.Height != height || width <= 0 || height <= 0 || desc.Format != gpu.TexRGBA8 && desc.Format != gpu.TexSRGBA8 {
return fmt.Errorf("shadow preview needs a valid RGBA8/SRGBA8 2D target matching %dx%d", width, height)
}
sourceDesc, ok := describer.TextureInfo(source)
if !ok || sourceDesc.Dimension != gpu.Texture2DArray || layer >= sourceDesc.LayerCount() {
return fmt.Errorf("shadow preview source is no longer a valid array layer")
}
area := mathx.Rect{W: float32(width), H: float32(height)}
pass := gpu.PassDesc{Target: target, Color: gpu.LoadClear, ClearRect: area, Label: "lighting.shadow-preview"}
if validator, ok := r.dev.(gpu.RenderTargetValidator); ok {
if err := validator.ValidateRenderTarget(pass); err != nil {
return fmt.Errorf("shadow preview target: %w", err)
}
}
if err := r.prepareShadowPreview(); err != nil {
return err
}
s := r.shadowPreview
binary.LittleEndian.PutUint32(s.data[:4], math.Float32bits(float32(layer)))
r.dev.UpdateBuffer(s.uniform, 0, s.data[:])
p := r.frame.BeginPass(pass)
p.SetViewport(area)
p.SetPipeline(s.pipe)
p.SetUniformBuffer(0, s.uniform)
p.SetTexture(0, source, s.sampler)
p.Draw(0, 4)
p.End()
return nil
}
func (r *Renderer) prepareShadowPreview() (err error) {
if r.shadowPreview != nil {
return nil
}
// The helper has no asset/importer dependency. It only visualizes a GPU
// resource already produced by the normal lighting renderer.
prefix := "#version 330 core\n"
if r.caps.ESSL {
prefix = "#version 300 es\nprecision highp float;\nprecision highp int;\nprecision highp sampler2DArray;\n"
}
s := &shadowPreviewState{}
r.shadowPreview = s
defer func() {
if err != nil {
r.closeShadowPreview()
}
}()
s.shader, err = r.dev.CreateShader(gpu.ShaderDesc{Label: "lighting.shadow-preview", Sources: []gpu.ShaderSource{
{Stage: gpu.StageVertex, Source: prefix + `
out vec2 vUV;
void main() {
vec2 p = vec2(float(gl_VertexID & 1), float(gl_VertexID >> 1));
vUV = p;
gl_Position = vec4(p * 2.0 - 1.0, 0.0, 1.0);
}`},
{Stage: gpu.StageFragment, Source: prefix + `
in vec2 vUV;
uniform sampler2DArray tex0;
layout(std140) uniform ub0 { vec4 options; };
out vec4 outColor;
void main() {
float visibility = texture(tex0, vec3(vUV, options.x)).r;
outColor = vec4(vec3(visibility), 1.0);
}`},
}})
if err != nil {
return fmt.Errorf("shadow preview shader: %w", err)
}
s.pipe, err = r.dev.CreatePipeline(gpu.PipelineDesc{Shader: s.shader, Primitive: gpu.PrimTriangleStrip, Label: "lighting.shadow-preview"})
if err != nil {
return err
}
s.uniform, err = r.dev.CreateBuffer(gpu.BufferDesc{Usage: gpu.BufferUniform, Size: len(s.data), Dynamic: true, Label: "lighting.shadow-preview"})
if err != nil {
return err
}
s.sampler, err = r.dev.CreateSampler(gpu.SamplerDesc{MinFilter: gpu.FilterLinear, MagFilter: gpu.FilterLinear})
return err
}
func (r *Renderer) closeShadowPreview() {
if r.shadowPreview == nil {
return
}
s := r.shadowPreview
if !s.pipe.IsZero() {
r.dev.DestroyPipeline(s.pipe)
}
if !s.shader.IsZero() {
r.dev.DestroyShader(s.shader)
}
if !s.uniform.IsZero() {
r.dev.DestroyBuffer(s.uniform)
}
if !s.sampler.IsZero() {
r.dev.DestroySampler(s.sampler)
}
r.shadowPreview = nil
}