zworld/engine/object/light/directional.go

237 lines
6.2 KiB
Go
Raw Normal View History

2024-01-14 22:56:06 +08:00
package light
import (
"zworld/engine/object"
"zworld/engine/render/uniform"
"zworld/engine/renderapi"
"zworld/engine/renderapi/color"
"zworld/plugins/math"
"zworld/plugins/math/mat4"
"zworld/plugins/math/vec3"
"zworld/plugins/math/vec4"
)
type DirectionalArgs struct {
Color color.T
Intensity float32
Shadows bool
Cascades int
}
type Cascade struct {
View mat4.T
Proj mat4.T
ViewProj mat4.T
NearSplit float32
FarSplit float32
}
type Directional struct {
object.Component
cascades []Cascade
Color object.Property[color.T]
Intensity object.Property[float32]
Shadows object.Property[bool]
CascadeLambda object.Property[float32]
CascadeBlend object.Property[float32]
}
var _ T = &Directional{}
func init() {
object.Register[*Directional](DeserializeDirectional)
}
func NewDirectional(args DirectionalArgs) *Directional {
lit := object.NewComponent(&Directional{
cascades: make([]Cascade, args.Cascades),
Color: object.NewProperty(args.Color),
Intensity: object.NewProperty(args.Intensity),
Shadows: object.NewProperty(args.Shadows),
CascadeLambda: object.NewProperty[float32](0.9),
CascadeBlend: object.NewProperty[float32](3.0),
})
return lit
}
func (lit *Directional) Name() string { return "DirectionalLight" }
func (lit *Directional) Type() Type { return TypeDirectional }
func (lit *Directional) CastShadows() bool { return lit.Shadows.Get() }
func farSplitDist(cascade, cascades int, near, far, splitLambda float32) float32 {
clipRange := far - near
minZ := near
maxZ := near + clipRange
rnge := maxZ - minZ
ratio := maxZ / minZ
// Calculate split depths based on view camera frustum
// Based on method presented in https://developer.nvidia.com/gpugems/GPUGems3/gpugems3_ch10.html
p := (float32(cascade) + 1) / float32(cascades)
log := minZ * math.Pow(ratio, p)
uniform := minZ + rnge*p
d := splitLambda*(log-uniform) + uniform
return (d - near) / clipRange
}
func nearSplitDist(cascade, cascades int, near, far, splitLambda float32) float32 {
if cascade == 0 {
return 0
}
return farSplitDist(cascade-1, cascades, near, far, splitLambda)
}
func (lit *Directional) PreDraw(args renderapi.Args, scene object.Object) error {
// update cascades
for i, _ := range lit.cascades {
lit.cascades[i] = lit.calculateCascade(args, i, len(lit.cascades))
}
return nil
}
func (lit *Directional) calculateCascade(args renderapi.Args, cascade, cascades int) Cascade {
texSize := float32(2048)
frustumCorners := []vec3.T{
vec3.New(-1, 1, -1), // NTL
vec3.New(1, 1, -1), // NTR
vec3.New(-1, -1, -1), // NBL
vec3.New(1, -1, -1), // NBR
vec3.New(-1, 1, 1), // FTL
vec3.New(1, 1, 1), // FTR
vec3.New(-1, -1, 1), // FBL
vec3.New(1, -1, 1), // FBR
}
// transform frustum into world space
for i, corner := range frustumCorners {
frustumCorners[i] = args.VPInv.TransformPoint(corner)
}
// squash
nearSplit := nearSplitDist(cascade, cascades, args.Near, args.Far, lit.CascadeLambda.Get())
farSplit := farSplitDist(cascade, cascades, args.Near, args.Far, lit.CascadeLambda.Get())
for i := 0; i < 4; i++ {
dist := frustumCorners[i+4].Sub(frustumCorners[i])
frustumCorners[i] = frustumCorners[i].Add(dist.Scaled(nearSplit))
frustumCorners[i+4] = frustumCorners[i].Add(dist.Scaled(farSplit))
}
// calculate frustum center
center := vec3.Zero
for _, corner := range frustumCorners {
center = center.Add(corner)
}
center = center.Scaled(float32(1) / 8)
radius := float32(0)
for _, corner := range frustumCorners {
distance := vec3.Distance(corner, center)
radius = math.Max(radius, distance)
}
radius = math.Snap(radius, 16)
// create light view matrix looking at the center of the
// camera frustum
ldir := lit.Transform().Forward()
position := center.Sub(ldir.Scaled(radius))
lview := mat4.LookAt(position, center, vec3.UnitY)
lproj := mat4.Orthographic(
-radius-0.01, radius+0.01,
-radius-0.01, radius+0.01,
0, 2*radius)
lvp := lproj.Mul(&lview)
// round the center of the lights projection to the nearest texel
origin := lvp.TransformPoint(vec3.New(0, 0, 0)).Scaled(texSize / 2.0)
offset := origin.Round().Sub(origin)
offset.Scale(2.0 / texSize)
lproj[12] = offset.X
lproj[13] = offset.Y
// re-create view-projection after rounding
lvp = lproj.Mul(&lview)
return Cascade{
Proj: lproj,
View: lview,
ViewProj: lvp,
NearSplit: nearSplit * args.Far,
FarSplit: farSplit * args.Far,
}
}
func (lit *Directional) LightData(shadowmaps ShadowmapStore) uniform.Light {
ldir := lit.Transform().Forward()
entry := uniform.Light{
Type: uint32(TypeDirectional),
Position: vec4.Extend(ldir, 0),
Color: lit.Color.Get(),
Intensity: lit.Intensity.Get(),
Range: lit.CascadeBlend.Get(),
}
for cascadeIndex, cascade := range lit.cascades {
entry.ViewProj[cascadeIndex] = cascade.ViewProj
entry.Distance[cascadeIndex] = cascade.FarSplit
if handle, exists := shadowmaps.Lookup(lit, cascadeIndex); exists {
entry.Shadowmap[cascadeIndex] = uint32(handle)
}
}
return entry
}
func (lit *Directional) Shadowmaps() int {
return len(lit.cascades)
}
func (lit *Directional) ShadowProjection(mapIndex int) uniform.Camera {
cascade := lit.cascades[mapIndex]
return uniform.Camera{
Proj: cascade.Proj,
View: cascade.View,
ViewProj: cascade.ViewProj,
ProjInv: cascade.Proj.Invert(),
ViewInv: cascade.View.Invert(),
ViewProjInv: cascade.ViewProj.Invert(),
Eye: vec4.Extend(lit.Transform().Position(), 0),
Forward: vec4.Extend(lit.Transform().Forward(), 0),
}
}
type DirectionalState struct {
object.ComponentState
DirectionalArgs
}
func (lit *Directional) Serialize(enc object.Encoder) error {
return enc.Encode(DirectionalState{
// send help
ComponentState: object.NewComponentState(lit.Component),
DirectionalArgs: DirectionalArgs{
Color: lit.Color.Get(),
Intensity: lit.Intensity.Get(),
Shadows: lit.Shadows.Get(),
},
})
}
func DeserializeDirectional(dec object.Decoder) (object.Component, error) {
var state DirectionalState
if err := dec.Decode(&state); err != nil {
return nil, err
}
obj := NewDirectional(state.DirectionalArgs)
obj.Component = state.ComponentState.New()
return obj, nil
}