zworld/engine/render/pass/ssao.go
2024-01-14 22:56:06 +08:00

268 lines
6.5 KiB
Go

package pass
import (
"fmt"
"github.com/vkngwrapper/core/v2/core1_0"
"unsafe"
"zworld/engine/object"
"zworld/engine/renderapi"
"zworld/engine/renderapi/command"
"zworld/engine/renderapi/descriptor"
"zworld/engine/renderapi/framebuffer"
"zworld/engine/renderapi/image"
"zworld/engine/renderapi/material"
"zworld/engine/renderapi/renderpass"
"zworld/engine/renderapi/renderpass/attachment"
"zworld/engine/renderapi/shader"
"zworld/engine/renderapi/texture"
"zworld/engine/renderapi/vertex"
"zworld/engine/renderapi/vulkan"
"zworld/plugins/math"
"zworld/plugins/math/mat4"
"zworld/plugins/math/random"
"zworld/plugins/math/vec3"
"zworld/plugins/math/vec4"
)
const SSAOSamples = 32
type AmbientOcclusionPass struct {
app vulkan.App
pass renderpass.T
fbuf framebuffer.Array
mat *material.Material[*AmbientOcclusionDescriptors]
desc []*material.Instance[*AmbientOcclusionDescriptors]
quad vertex.Mesh
scale float32
position []texture.T
normal []texture.T
kernel [SSAOSamples]vec4.T
noise *HemisphereNoise
}
var _ Pass = &AmbientOcclusionPass{}
type AmbientOcclusionParams struct {
Projection mat4.T
Kernel [SSAOSamples]vec4.T
Samples int32
Scale float32
Radius float32
Bias float32
Power float32
}
type AmbientOcclusionDescriptors struct {
descriptor.Set
Position *descriptor.Sampler
Normal *descriptor.Sampler
Noise *descriptor.Sampler
Params *descriptor.Uniform[AmbientOcclusionParams]
}
func NewAmbientOcclusionPass(app vulkan.App, target vulkan.Target, gbuffer GeometryBuffer) *AmbientOcclusionPass {
var err error
p := &AmbientOcclusionPass{
app: app,
scale: float32(gbuffer.Width()) / float32(target.Width()),
}
p.pass = renderpass.New(app.Device(), renderpass.Args{
Name: "AmbientOcclusion",
ColorAttachments: []attachment.Color{
{
Name: OutputAttachment,
Image: attachment.FromImageArray(target.Surfaces()),
LoadOp: core1_0.AttachmentLoadOpDontCare,
StoreOp: core1_0.AttachmentStoreOpStore,
FinalLayout: core1_0.ImageLayoutShaderReadOnlyOptimal,
},
},
Subpasses: []renderpass.Subpass{
{
Name: MainSubpass,
Depth: false,
ColorAttachments: []attachment.Name{OutputAttachment},
},
},
})
p.mat = material.New(
app.Device(),
material.Args{
Shader: app.Shaders().Fetch(shader.NewRef("ssao")),
Pass: p.pass,
Pointers: vertex.ParsePointers(vertex.T{}),
DepthTest: false,
DepthWrite: false,
},
&AmbientOcclusionDescriptors{
Position: &descriptor.Sampler{
Stages: core1_0.StageFragment,
},
Normal: &descriptor.Sampler{
Stages: core1_0.StageFragment,
},
Noise: &descriptor.Sampler{
Stages: core1_0.StageFragment,
},
Params: &descriptor.Uniform[AmbientOcclusionParams]{
Stages: core1_0.StageFragment,
},
})
p.fbuf, err = framebuffer.NewArray(target.Frames(), app.Device(), "ssao", target.Width(), target.Height(), p.pass)
if err != nil {
panic(err)
}
p.quad = vertex.ScreenQuad("ssao-pass-quad")
// create noise texture
p.noise = NewHemisphereNoise(4, 4)
// create sampler kernel
p.kernel = [SSAOSamples]vec4.T{}
for i := 0; i < len(p.kernel); i++ {
var sample vec3.T
for {
sample = vec3.Random(
vec3.New(-1, 0, -1),
vec3.New(1, 1, 1),
)
if sample.LengthSqr() > 1 {
continue
}
sample = sample.Normalized()
if vec3.Dot(sample, vec3.Up) < 0.5 {
continue
}
sample = sample.Scaled(random.Range(0, 1))
break
}
// we dont want a uniform sample distribution
// push samples closer to the origin
scale := float32(i) / float32(SSAOSamples)
scale = math.Lerp(0.1, 1.0, scale*scale)
sample = sample.Scaled(scale)
p.kernel[i] = vec4.Extend(sample, 0)
}
// todo: if we shuffle the kernel, it would be ok to use fewer samples
p.desc = p.mat.InstantiateMany(app.Pool(), target.Frames())
p.position = make([]texture.T, target.Frames())
p.normal = make([]texture.T, target.Frames())
for i := 0; i < target.Frames(); i++ {
posKey := fmt.Sprintf("ssao-position-%d", i)
p.position[i], err = texture.FromImage(app.Device(), posKey, gbuffer.Position()[i], texture.Args{
Filter: texture.FilterNearest,
Wrap: texture.WrapClamp,
})
if err != nil {
// todo: clean up
panic(err)
}
p.desc[i].Descriptors().Position.Set(p.position[i])
normKey := fmt.Sprintf("ssao-normal-%d", i)
p.normal[i], err = texture.FromImage(app.Device(), normKey, gbuffer.Normal()[i], texture.Args{
Filter: texture.FilterNearest,
Wrap: texture.WrapClamp,
})
if err != nil {
// todo: clean up
panic(err)
}
p.desc[i].Descriptors().Normal.Set(p.normal[i])
}
return p
}
func (p *AmbientOcclusionPass) Record(cmds command.Recorder, args renderapi.Args, scene object.Component) {
quad := p.app.Meshes().Fetch(p.quad)
cmds.Record(func(cmd command.Buffer) {
cmd.CmdBeginRenderPass(p.pass, p.fbuf[args.Frame])
p.desc[args.Frame].Bind(cmd)
p.desc[args.Frame].Descriptors().Noise.Set(p.app.Textures().Fetch(p.noise))
p.desc[args.Frame].Descriptors().Params.Set(AmbientOcclusionParams{
Projection: args.Projection,
Kernel: p.kernel,
Samples: 32,
Scale: p.scale,
Radius: 0.4,
Bias: 0.02,
Power: 2.6,
})
quad.Draw(cmd, 0)
cmd.CmdEndRenderPass()
})
}
func (p *AmbientOcclusionPass) Destroy() {
p.pass.Destroy()
p.fbuf.Destroy()
for i := 0; i < len(p.position); i++ {
p.position[i].Destroy()
p.normal[i].Destroy()
}
p.mat.Destroy()
}
func (p *AmbientOcclusionPass) Name() string {
return "AmbientOcclusion"
}
type HemisphereNoise struct {
Width int
Height int
key string
}
func NewHemisphereNoise(width, height int) *HemisphereNoise {
return &HemisphereNoise{
key: fmt.Sprintf("noise-hemisphere-%dx%d", width, height),
Width: width,
Height: height,
}
}
func (n *HemisphereNoise) Key() string { return n.key }
func (n *HemisphereNoise) Version() int { return 1 }
func (n *HemisphereNoise) ImageData() *image.Data {
buffer := make([]vec4.T, 4*n.Width*n.Height)
for i := range buffer {
buffer[i] = vec4.Extend(vec3.Random(
vec3.New(-1, -1, 0),
vec3.New(1, 1, 0),
).Normalized(), 0)
}
// cast to byte array
ptr := (*byte)(unsafe.Pointer(&buffer[0]))
bytes := unsafe.Slice(ptr, int(unsafe.Sizeof(vec4.T{}))*len(buffer))
return &image.Data{
Width: n.Width,
Height: n.Height,
Format: core1_0.FormatR32G32B32A32SignedFloat,
Buffer: bytes,
}
}
func (n *HemisphereNoise) TextureArgs() texture.Args {
return texture.Args{
Filter: texture.FilterNearest,
Wrap: texture.WrapRepeat,
}
}