Files
aego/tests/lightinggolden/reference.go
T
sanspie 5eb1832907
ci-platforms / darwin amd64 smoke (push) Skipped
ci / build (amd64, linux, ubuntu-24.04) (push) Failing after 15m28s
ci / release (push) Skipped
ci-platforms / windows amd64 smoke (push) Skipped
ci-platforms / darwin arm64 smoke (push) Skipped
ci / checks (push) Successful in 7m47s
Complete Stage 12 for v0.12.0
2026-11-05 11:46:53 +03:00

273 lines
7.7 KiB
Go

package lightinggolden
import (
"math"
"aego/core/mathx"
"aego/render/lighting"
)
// Surface is an independent CPU description of a rectangular test receiver.
// Basis contains its full local-to-world linear transform, column major.
type Surface struct {
X, Y, Width, Height float64
Basis [4]float64
Color [4]float64 // linear, already premultiplied and vertex-quantized
Normal [3]float64
Emission [3]float64
Lit, Additive bool
// NormalAt allows independently trimmed/frame-mapped normal images.
NormalAt func(u, v float64) [3]float64
EmissionAt func(u, v float64) [3]float64
}
func WhiteSurface() Surface {
return Surface{X: 16, Y: 16, Width: 24, Height: 24, Basis: [4]float64{1, 0, 0, 1}, Color: [4]float64{1, 1, 1, 1}, Normal: [3]float64{0, 0, 1}, Lit: true}
}
func PMA(c mathx.Color) [4]float64 {
// Sprite tints are intentionally stored as normalized RGBA8 in the ABI.
a := float64(c.A)
return [4]float64{quant(float64(c.R) * a), quant(float64(c.G) * a), quant(float64(c.B) * a), quant(a)}
}
func normalize(v [3]float64) [3]float64 {
l := math.Sqrt(v[0]*v[0] + v[1]*v[1] + v[2]*v[2])
if l < 1e-10 {
return [3]float64{0, 0, 1}
}
return [3]float64{v[0] / l, v[1] / l, v[2] / l}
}
// NormalReference performs the image-up to world-down conversion followed by
// the inverse transpose of the surface's model matrix. Size does not rotate or
// rescale a normal; nonuniform node scale does.
func NormalReference(rgb [3]byte, basis [4]float64, flipX, flipY bool) [3]float64 {
x, y, z := float64(rgb[0])*2/255-1, -(float64(rgb[1])*2/255 - 1), float64(rgb[2])*2/255-1
if flipX {
x = -x
}
if flipY {
y = -y
}
det := basis[0]*basis[3] - basis[1]*basis[2]
if math.Abs(det) < 1e-8 {
return [3]float64{0, 0, 1}
}
return normalize([3]float64{(basis[3]*x - basis[1]*y) / det, (-basis[2]*x + basis[0]*y) / det, z})
}
func clamp01(x float64) float64 { return math.Max(0, math.Min(1, x)) }
func quant(x float64) float64 { return math.Floor(clamp01(x)*255+.5) / 255 }
func srgb(x float64) float64 {
x = clamp01(x)
if x <= .0031308 {
return 12.92 * x
}
return 1.055*math.Pow(x, 1/2.4) - .055
}
func smooth(a, b, x float64) float64 {
if a == b {
if x >= a {
return 1
}
return 0
}
t := clamp01((x - a) / (b - a))
return t * t * (3 - 2*t)
}
// Reference draws in submission order; it neither groups by material nor uses
// renderer batching, GPU packing, shadow triangles or the generated GLSL.
func Reference(in lighting.Input, background [4]float64, surfaces ...Surface) []byte {
out := make([]byte, Size*Size*4)
e := in.Environment
if !in.HasEnvironment {
e = lighting.DefaultEnvironment()
}
for y := 0; y < Size; y++ {
for x := 0; x < Size; x++ {
p := background
if e.ColorMode == lighting.LDR {
for c := range p {
p[c] = quant(p[c])
}
}
wx, wy := float64(x)+.5, float64(y)+.5
for _, s := range surfaces {
b := s.Basis
det := b[0]*b[3] - b[1]*b[2]
if math.Abs(det) < 1e-8 {
continue
}
dx, dy := wx-s.X, wy-s.Y
lx, ly := (b[3]*dx-b[2]*dy)/det, (-b[1]*dx+b[0]*dy)/det
u, v := lx/s.Width+.5, ly/s.Height+.5
if u < 0 || u >= 1 || v < 0 || v >= 1 {
continue
}
n := s.Normal
if s.NormalAt != nil {
n = s.NormalAt(u, v)
}
src := s.Color
emission := s.Emission
if s.EmissionAt != nil {
emission = s.EmissionAt(u, v)
}
if s.Lit {
var illumination [3]float64
switch in.Debug {
case lighting.DebugNormals:
for c := 0; c < 3; c++ {
src[c] = (n[c]*.5 + .5) * src[3]
}
case lighting.DebugShadows:
visible := 1.0
for _, l := range in.Lights {
if l.Shadows {
visible = math.Min(visible, shadowVisible(in, l, wx, wy))
}
}
for c := 0; c < 3; c++ {
src[c] = visible * src[3]
}
default:
illumination = [3]float64{float64(e.Ambient.R * e.AmbientIntensity), float64(e.Ambient.G * e.AmbientIntensity), float64(e.Ambient.B * e.AmbientIntensity)}
if in.Disabled {
illumination = [3]float64{1, 1, 1}
} else {
for _, l := range in.Lights {
energy := lightEnergy(in, l, wx, wy, n)
illumination[0] += float64(l.Color.R) * energy
illumination[1] += float64(l.Color.G) * energy
illumination[2] += float64(l.Color.B) * energy
}
}
for c := 0; c < 3; c++ {
src[c] = src[c]*illumination[c] + emission[c]*src[3]
}
}
}
if s.Additive {
for c := 0; c < 3; c++ {
p[c] += src[c]
}
} else {
for c := 0; c < 4; c++ {
p[c] = src[c] + p[c]*(1-src[3])
}
}
if e.ColorMode == lighting.LDR {
for c := range p {
p[c] = quant(p[c])
}
}
}
a := clamp01(p[3])
exposure := float64(e.Exposure)
if in.Debug != lighting.DebugLighting {
exposure = 1
}
for c := 0; c < 3; c++ {
straight := 0.0
if a > 1e-8 {
straight = math.Max(0, p[c]) / a * exposure
}
if e.ColorMode == lighting.HDR && in.Debug == lighting.DebugLighting {
straight /= 1 + straight
}
out[(y*Size+x)*4+c] = byte(math.Floor(clamp01(srgb(straight)*a)*255 + .5))
}
out[(y*Size+x)*4+3] = byte(math.Floor(a*255 + .5))
}
}
return out
}
func lightEnergy(in lighting.Input, l lighting.Light, x, y float64, n [3]float64) float64 {
var direction [3]float64
atten := 1.0
if l.Type == lighting.Directional {
ce := math.Cos(float64(l.Elevation))
direction = [3]float64{-float64(l.Direction.X) * ce, -float64(l.Direction.Y) * ce, math.Sin(float64(l.Elevation))}
} else {
dx, dy := float64(l.Position.X)-x, float64(l.Position.Y)-y
d := math.Hypot(dx, dy)
r := float64(l.Range)
if d >= r {
return 0
}
atten = math.Pow(math.Max(0, 1-d/r), float64(l.Falloff))
direction = normalize([3]float64{dx, dy, float64(l.Height)})
if l.Type == lighting.Spot {
cosine := 1.0
if d > 1e-6 {
cosine = (-dx*float64(l.Direction.X) - dy*float64(l.Direction.Y)) / d
}
atten *= smooth(math.Cos(float64(l.OuterAngle)/2), math.Cos(float64(l.InnerAngle)/2), cosine)
}
if l.Type == lighting.Sprite {
if l.Cookie.IsZero() {
return 0
}
// The test cookie is a white 2:1 image. Its own aspect is kept inside
// the square range: letterbox areas carry zero intensity.
u := (-dx*float64(l.Direction.X)-dy*float64(l.Direction.Y))/(2*r) + .5
v := (dx*float64(l.Direction.Y)-dy*float64(l.Direction.X))/(2*r) + .5
if u < 0 || u > 1 || v < .25 || v > .75 {
return 0
}
}
if l.Shadows {
atten *= shadowVisible(in, l, x, y)
}
}
dot := math.Max(0, n[0]*direction[0]+n[1]*direction[1]+n[2]*direction[2])
return float64(l.Intensity) * atten * dot
}
func cross(ax, ay, bx, by float64) float64 { return ax*by - ay*bx }
func shadowVisible(in lighting.Input, l lighting.Light, x, y float64) float64 {
px, py := float64(l.Position.X), float64(l.Position.Y)
rx, ry := x-px, y-py
for _, o := range in.Occluders {
if o.Layers&l.ShadowCasterMask == 0 || o.Count < 2 {
continue
}
pts := in.Points[o.First : o.First+o.Count]
if o.Closed {
inside := false
for i, a := range pts {
b := pts[(i+1)%len(pts)]
ax, ay, bx, by := float64(a.X), float64(a.Y), float64(b.X), float64(b.Y)
if (ay > py) != (by > py) && px < (bx-ax)*(py-ay)/(by-ay)+ax {
inside = !inside
}
}
if inside {
return 0
}
}
n := len(pts) - 1
if o.Closed {
n = len(pts)
}
for i := 0; i < n; i++ {
a, b := pts[i], pts[(i+1)%len(pts)]
sx, sy := float64(b.X-a.X), float64(b.Y-a.Y)
ax, ay := float64(a.X)-px, float64(a.Y)-py
den := cross(rx, ry, sx, sy)
if math.Abs(den) < 1e-10 {
continue
}
t, u := cross(ax, ay, sx, sy)/den, cross(ax, ay, rx, ry)/den
if t > 0 && t < 1 && u >= 0 && u <= 1 {
return 0
}
}
}
return 1
}