From b67f30942b8559c11c682c022a6f8966e5adcac8 Mon Sep 17 00:00:00 2001 From: Momchil Atanasov Date: Sun, 8 Sep 2024 11:10:37 +0300 Subject: [PATCH] Rendering improvements (#57) --- game/asset/dsl/operation_light.go | 26 + game/asset/dsl/provider_image.go | 12 +- game/asset/dsl/provider_model.go | 46 +- game/asset/dsl/provider_texture.go | 38 +- game/controller.go | 53 +- game/ecs/engine.go | 2 +- game/ecs/option.go | 7 + game/graphics/camera.go | 37 +- game/graphics/debug.go | 4 +- game/graphics/engine.go | 36 +- game/graphics/internal/shadow.go | 32 + game/graphics/internal/uniform.go | 17 +- game/graphics/internal/uniform_lighting.go | 36 +- .../{ambient_light.go => light_ambient.go} | 5 + ...ectional_light.go => light_directional.go} | 46 +- .../{point_light.go => light_point.go} | 0 .../graphics/{spot_light.go => light_spot.go} | 0 game/graphics/option.go | 95 ++ game/graphics/renderer.go | 1424 +---------------- game/graphics/scene.go | 2 +- game/graphics/stage.go | 97 ++ game/graphics/stage_bloom.go | 107 +- game/graphics/stage_builder.go | 61 + game/graphics/stage_common.go | 192 ++- game/graphics/stage_depth_source.go | 64 + game/graphics/stage_forward.go | 270 ++++ game/graphics/stage_forward_source.go | 69 + game/graphics/stage_geometry.go | 122 ++ game/graphics/stage_geometry_source.go | 84 + game/graphics/stage_lighting.go | 475 ++++++ game/graphics/stage_mesh.go | 213 +++ game/graphics/stage_probe.go | 204 +++ game/graphics/stage_provider.go | 74 + game/graphics/stage_shadow.go | 289 ++++ game/graphics/stage_tonemap.go | 148 ++ game/hierarchy/node.go | 5 +- game/physics/engine.go | 10 +- game/physics/option.go | 18 + game/preset/camera.go | 4 + game/scene.go | 1 - go.mod | 18 +- go.sum | 36 +- render/api.go | 4 + render/command.go | 3 + render/framebuffer.go | 32 +- render/pass.go | 7 + render/texture.go | 26 + ui/font_factory.go | 7 +- ui/std/viewport.go | 4 +- util/async/worker.go | 14 +- util/gltfutil/util.go | 18 +- 51 files changed, 3028 insertions(+), 1566 deletions(-) create mode 100644 game/ecs/option.go create mode 100644 game/graphics/internal/shadow.go rename game/graphics/{ambient_light.go => light_ambient.go} (93%) rename game/graphics/{directional_light.go => light_directional.go} (78%) rename game/graphics/{point_light.go => light_point.go} (100%) rename game/graphics/{spot_light.go => light_spot.go} (100%) create mode 100644 game/graphics/option.go create mode 100644 game/graphics/stage.go create mode 100644 game/graphics/stage_builder.go create mode 100644 game/graphics/stage_depth_source.go create mode 100644 game/graphics/stage_forward.go create mode 100644 game/graphics/stage_forward_source.go create mode 100644 game/graphics/stage_geometry.go create mode 100644 game/graphics/stage_geometry_source.go create mode 100644 game/graphics/stage_lighting.go create mode 100644 game/graphics/stage_mesh.go create mode 100644 game/graphics/stage_probe.go create mode 100644 game/graphics/stage_provider.go create mode 100644 game/graphics/stage_shadow.go create mode 100644 game/graphics/stage_tonemap.go create mode 100644 game/physics/option.go diff --git a/game/asset/dsl/operation_light.go b/game/asset/dsl/operation_light.go index 518f0874..90f5ef12 100644 --- a/game/asset/dsl/operation_light.go +++ b/game/asset/dsl/operation_light.go @@ -170,3 +170,29 @@ func SetRefractionTexture(textureProvider Provider[*mdl.Texture]) Operation { }, ) } + +// SetCastShadow configures the cast shadow of the target. +func SetCastShadow(castShadowProvider Provider[bool]) Operation { + return FuncOperation( + // apply function + func(target any) error { + castShadow, err := castShadowProvider.Get() + if err != nil { + return fmt.Errorf("error getting cast shadow: %w", err) + } + + caster, ok := target.(mdl.ShadowCaster) + if !ok { + return fmt.Errorf("target %T is not a shadow caster", target) + } + caster.SetCastShadow(castShadow) + + return nil + }, + + // digest function + func() ([]byte, error) { + return CreateDigest("set-cast-shadow", castShadowProvider) + }, + ) +} diff --git a/game/asset/dsl/provider_image.go b/game/asset/dsl/provider_image.go index 1b6e9f46..f272dd17 100644 --- a/game/asset/dsl/provider_image.go +++ b/game/asset/dsl/provider_image.go @@ -130,14 +130,16 @@ func ResizedCubeImage(imageProvider Provider[*mdl.CubeImage], newSizeProvider Pr // IrradianceCubeImage creates an irradiance cube image from the provided // HDR skybox cube image. func IrradianceCubeImage(imageProvider Provider[*mdl.CubeImage], opts ...Operation) Provider[*mdl.CubeImage] { - var cfg irradianceConfig - for _, opt := range opts { - opt.Apply(&cfg) - } - return OnceProvider(FuncProvider( // get function func() (*mdl.CubeImage, error) { + var cfg irradianceConfig + for _, opt := range opts { + if err := opt.Apply(&cfg); err != nil { + return nil, fmt.Errorf("failed to configure irradiance cube image: %w", err) + } + } + image, err := imageProvider.Get() if err != nil { return nil, fmt.Errorf("error getting image: %w", err) diff --git a/game/asset/dsl/provider_model.go b/game/asset/dsl/provider_model.go index e71731a5..d1344c97 100644 --- a/game/asset/dsl/provider_model.go +++ b/game/asset/dsl/provider_model.go @@ -15,7 +15,7 @@ import ( "github.com/mokiat/lacking/game/asset/mdl" "github.com/mokiat/lacking/util/gltfutil" "github.com/qmuntal/gltf" - lightspunctual "github.com/qmuntal/gltf/ext/lightspuntual" + "github.com/qmuntal/gltf/ext/lightspunctual" ) // CreateModel creates a new model with the specified name and operations. @@ -55,7 +55,7 @@ func OpenGLTFModel(path string, opts ...Operation) Provider[*mdl.Model] { var cfg openGLTFModelConfig for _, opt := range opts { if err := opt.Apply(&cfg); err != nil { - return nil, err + return nil, fmt.Errorf("failed to configure gltf model: %w", err) } } @@ -116,17 +116,17 @@ func BuildModelResource(gltfDoc *gltf.Document, forceCollision bool) (*mdl.Model model := &mdl.Model{} // build images - imagesFromIndex := make(map[uint32]*mdl.Image) + imagesFromIndex := make(map[int]*mdl.Image) for i, gltfImage := range gltfDoc.Images { img, err := openGLTFImage(gltfDoc, gltfImage) if err != nil { return nil, fmt.Errorf("error loading image: %w", err) } - imagesFromIndex[uint32(i)] = img + imagesFromIndex[i] = img } // build textures - texturesFromIndex := make(map[uint32]*mdl.Texture) + texturesFromIndex := make(map[int]*mdl.Texture) for i, img := range imagesFromIndex { texture := &mdl.Texture{} texture.SetName(img.Name()) @@ -139,7 +139,7 @@ func BuildModelResource(gltfDoc *gltf.Document, forceCollision bool) (*mdl.Model } // build samplers - samplersFromIndex := make(map[uint32]*mdl.Sampler) + samplersFromIndex := make(map[int]*mdl.Sampler) for i, gltfTexture := range gltfDoc.Textures { sampler := &mdl.Sampler{} if gltfTexture.Sampler != nil { @@ -186,11 +186,11 @@ func BuildModelResource(gltfDoc *gltf.Document, forceCollision bool) (*mdl.Model } else { return nil, fmt.Errorf("texture source not set") } - samplersFromIndex[uint32(i)] = sampler + samplersFromIndex[i] = sampler } // build materials - materialFromIndex := make(map[uint32]*mdl.Material) + materialFromIndex := make(map[int]*mdl.Material) for i, gltfMaterial := range gltfDoc.Materials { var ( color sprec.Vec4 @@ -199,9 +199,9 @@ func BuildModelResource(gltfDoc *gltf.Document, forceCollision bool) (*mdl.Model normalScale float32 alphaThreshold float32 - colorTextureIndex *uint32 - metallicRoughnessTextureIndex *uint32 - normalTextureIndex *uint32 + colorTextureIndex *int + metallicRoughnessTextureIndex *int + normalTextureIndex *int ) if gltfPBR := gltfMaterial.PBRMetallicRoughness; gltfPBR != nil { @@ -288,12 +288,12 @@ func BuildModelResource(gltfDoc *gltf.Document, forceCollision bool) (*mdl.Model material.SetSampler("normalSampler", samplersFromIndex[*normalTextureIndex]) } - materialFromIndex[uint32(i)] = material + materialFromIndex[i] = material } // build mesh definitions - meshDefinitionFromIndex := make(map[uint32]*mdl.MeshDefinition) - bodyDefinitionFromIndex := make(map[uint32]*mdl.BodyDefinition) + meshDefinitionFromIndex := make(map[int]*mdl.MeshDefinition) + bodyDefinitionFromIndex := make(map[int]*mdl.BodyDefinition) for i, gltfMesh := range gltfDoc.Meshes { bodyMaterial := mdl.NewBodyMaterial() bodyDefinition := mdl.NewBodyDefinition(bodyMaterial) @@ -448,17 +448,17 @@ func BuildModelResource(gltfDoc *gltf.Document, forceCollision bool) (*mdl.Model meshDefinition.SetName(gltfMesh.Name) meshDefinition.SetGeometry(geometry) - meshDefinitionFromIndex[uint32(i)] = meshDefinition + meshDefinitionFromIndex[i] = meshDefinition if (geometry.Metadata().HasCollision() || forceCollision) && len(bodyDefinition.CollisionMeshes()) > 0 { - bodyDefinitionFromIndex[uint32(i)] = bodyDefinition + bodyDefinitionFromIndex[i] = bodyDefinition } } // prepare armatures - armatureFromIndex := make(map[uint32]*mdl.Armature) + armatureFromIndex := make(map[int]*mdl.Armature) for i := range gltfDoc.Skins { - armatureFromIndex[uint32(i)] = mdl.NewArmature() + armatureFromIndex[i] = mdl.NewArmature() } createPointLight := func(gltfLight *lightspunctual.Light) *mdl.PointLight { @@ -544,9 +544,9 @@ func BuildModelResource(gltfDoc *gltf.Document, forceCollision bool) (*mdl.Model } // build nodes - nodeFromIndex := make(map[uint32]*mdl.Node) - var visitNode func(nodeIndex uint32) *mdl.Node - visitNode = func(nodeIndex uint32) *mdl.Node { + nodeFromIndex := make(map[int]*mdl.Node) + var visitNode func(nodeIndex int) *mdl.Node + visitNode = func(nodeIndex int) *mdl.Node { gltfNode := gltfDoc.Nodes[nodeIndex] node := mdl.NewNode(gltfNode.Name) @@ -599,7 +599,7 @@ func BuildModelResource(gltfDoc *gltf.Document, forceCollision bool) (*mdl.Model // finalize armatures (now that all nodes are available) for i, gltfSkin := range gltfDoc.Skins { - armature := armatureFromIndex[uint32(i)] + armature := armatureFromIndex[i] for j, gltfJoint := range gltfSkin.Joints { joint := mdl.NewJoint() joint.SetNode(nodeFromIndex[gltfJoint]) @@ -610,7 +610,7 @@ func BuildModelResource(gltfDoc *gltf.Document, forceCollision bool) (*mdl.Model // prepare animations for _, gltfAnimation := range gltfDoc.Animations { - bindingFromNodeIndex := make(map[uint32]*mdl.AnimationBinding) + bindingFromNodeIndex := make(map[int]*mdl.AnimationBinding) animation := &mdl.Animation{} animation.SetName(gltfAnimation.Name) for _, gltfChannel := range gltfAnimation.Channels { diff --git a/game/asset/dsl/provider_texture.go b/game/asset/dsl/provider_texture.go index 800994ca..a9c86a91 100644 --- a/game/asset/dsl/provider_texture.go +++ b/game/asset/dsl/provider_texture.go @@ -10,14 +10,16 @@ import ( // Create2DTexture creates a new 2D texture with the specified format and // source image. func Create2DTexture(imageProvider Provider[*mdl.Image], opts ...Operation) Provider[*mdl.Texture] { - var cfg textureConfig - for _, opt := range opts { - opt.Apply(&cfg) - } - return OnceProvider(FuncProvider( // get function func() (*mdl.Texture, error) { + var cfg textureConfig + for _, opt := range opts { + if err := opt.Apply(&cfg); err != nil { + return nil, fmt.Errorf("failed to configure 2D texture: %w", err) + } + } + image, err := imageProvider.Get() if err != nil { return nil, fmt.Errorf("failed to get image: %w", err) @@ -27,7 +29,7 @@ func Create2DTexture(imageProvider Provider[*mdl.Image], opts ...Operation) Prov texture.SetName(image.Name()) texture.SetKind(mdl.TextureKind2D) texture.SetFormat(cfg.format.ValueOrDefault(mdl.TextureFormatRGBA8)) - texture.SetGenerateMipmaps(true) + texture.SetGenerateMipmaps(cfg.mipmapping) texture.Resize(image.Width(), image.Height()) texture.SetLayerImage(0, image) return &texture, nil @@ -43,14 +45,16 @@ func Create2DTexture(imageProvider Provider[*mdl.Image], opts ...Operation) Prov // CreateCubeTexture creates a new cube texture with the specified format and // source image. func CreateCubeTexture(cubeImageProvider Provider[*mdl.CubeImage], opts ...Operation) Provider[*mdl.Texture] { - var cfg textureConfig - for _, opt := range opts { - opt.Apply(&cfg) - } - return OnceProvider(FuncProvider( // get function func() (*mdl.Texture, error) { + var cfg textureConfig + for _, opt := range opts { + if err := opt.Apply(&cfg); err != nil { + return nil, fmt.Errorf("failed to configure cube texture: %w", err) + } + } + cubeImage, err := cubeImageProvider.Get() if err != nil { return nil, fmt.Errorf("failed to get cube image: %w", err) @@ -66,6 +70,7 @@ func CreateCubeTexture(cubeImageProvider Provider[*mdl.CubeImage], opts ...Opera var texture mdl.Texture texture.SetKind(mdl.TextureKindCube) texture.SetFormat(cfg.format.ValueOrDefault(mdl.TextureFormatRGBA16F)) + texture.SetGenerateMipmaps(cfg.mipmapping) texture.Resize(frontImage.Width(), frontImage.Height()) texture.SetLayerImage(0, frontImage) texture.SetLayerImage(1, rearImage) @@ -84,11 +89,20 @@ func CreateCubeTexture(cubeImageProvider Provider[*mdl.CubeImage], opts ...Opera } type textureConfig struct { - format opt.T[mdl.TextureFormat] + format opt.T[mdl.TextureFormat] + mipmapping bool } func (c *textureConfig) SetFormat(format mdl.TextureFormat) { c.format = opt.V(format) } +func (c *textureConfig) Mipmapping() bool { + return c.mipmapping +} + +func (c *textureConfig) SetMipmapping(mipmapping bool) { + c.mipmapping = mipmapping +} + var defaultCubeTextureProvider = CreateCubeTexture(defaultCubeImageProvider) diff --git a/game/controller.go b/game/controller.go index 4ce87a64..1b5bce83 100644 --- a/game/controller.go +++ b/game/controller.go @@ -1,8 +1,6 @@ package game import ( - "time" - "github.com/mokiat/lacking/app" "github.com/mokiat/lacking/debug/metric" "github.com/mokiat/lacking/game/asset" @@ -12,6 +10,11 @@ import ( "github.com/mokiat/lacking/util/async" ) +// NewController creates a new game controller that manages the lifecycle +// of a game engine. The controller will use the provided asset registry +// to load and manage assets. The provided shader collection will be used +// to render the game. The provided shader builder will be used to create +// new shaders when needed. func NewController(registry *asset.Registry, shaders graphics.ShaderCollection, shaderBuilder graphics.ShaderBuilder) *Controller { return &Controller{ registry: registry, @@ -22,6 +25,9 @@ func NewController(registry *asset.Registry, shaders graphics.ShaderCollection, var _ app.Controller = (*Controller)(nil) +// Controller is an implementation of the app.Controller interface which +// initializes a game engine and manages its lifecycle. Furthermore, it +// ensures that the game engine is updated and rendered on each frame. type Controller struct { app.NopController @@ -29,9 +35,14 @@ type Controller struct { shaders graphics.ShaderCollection shaderBuilder graphics.ShaderBuilder - gfxEngine *graphics.Engine - ecsEngine *ecs.Engine - physicsEngine *physics.Engine + gfxOptions []graphics.Option + gfxEngine *graphics.Engine + + ecsOptions []ecs.Option + ecsEngine *ecs.Engine + + physicsOptions []physics.Option + physicsEngine *physics.Engine window app.Window ioWorker *async.Worker @@ -40,19 +51,45 @@ type Controller struct { viewport graphics.Viewport } +// Registry returns the asset registry to be used by the game. func (c *Controller) Registry() *asset.Registry { return c.registry } +// UseGraphicsOptions allows to specify options that will be used +// when initializing the graphics engine. This method should be +// called before the controller is initialized by the app framework. +func (c *Controller) UseGraphicsOptions(opts ...graphics.Option) { + c.gfxOptions = opts +} + +// UseECSOptions allows to specify options that will be used +// when initializing the ECS engine. This method should be +// called before the controller is initialized by the app framework. +func (c *Controller) UseECSOptions(opts ...ecs.Option) { + c.ecsOptions = opts +} + +// UsePhysicsOptions allows to specify options that will be used +// when initializing the physics engine. This method should be +// called before the controller is initialized by the app framework. +func (c *Controller) UsePhysicsOptions(opts ...physics.Option) { + c.physicsOptions = opts +} + +// Engine returns the game engine that is managed by the controller. +// +// This method should only be called after the controller has been +// initialized by the app framework. func (c *Controller) Engine() *Engine { return c.engine } func (c *Controller) OnCreate(window app.Window) { c.window = window - c.gfxEngine = graphics.NewEngine(window.RenderAPI(), c.shaders, c.shaderBuilder) - c.ecsEngine = ecs.NewEngine() - c.physicsEngine = physics.NewEngine(16 * time.Millisecond) + c.gfxEngine = graphics.NewEngine(window.RenderAPI(), c.shaders, c.shaderBuilder, c.gfxOptions...) + c.ecsEngine = ecs.NewEngine(c.ecsOptions...) + c.physicsEngine = physics.NewEngine(c.physicsOptions...) c.ioWorker = async.NewWorker(4) go c.ioWorker.ProcessAll() diff --git a/game/ecs/engine.go b/game/ecs/engine.go index 0a74d6db..3e13ad37 100644 --- a/game/ecs/engine.go +++ b/game/ecs/engine.go @@ -1,7 +1,7 @@ package ecs // NewEngine creates a new ECS engine. -func NewEngine() *Engine { +func NewEngine(opts ...Option) *Engine { return &Engine{} } diff --git a/game/ecs/option.go b/game/ecs/option.go new file mode 100644 index 00000000..3da52d4b --- /dev/null +++ b/game/ecs/option.go @@ -0,0 +1,7 @@ +package ecs + +// Option is a configuration function that can be used to customize the +// behavior of the ECS engine. +type Option func(*config) + +type config struct{} diff --git a/game/graphics/camera.go b/game/graphics/camera.go index 47ac9605..9f1513fe 100644 --- a/game/graphics/camera.go +++ b/game/graphics/camera.go @@ -26,9 +26,10 @@ const ( FoVModePixelBased FoVMode = "pixel-based" ) -func newCamera() *Camera { +func newCamera(scene *Scene) *Camera { return &Camera{ Node: newNode(), + scene: scene, fov: sprec.Degrees(120), fovMode: FoVModeHorizontalPlus, near: 0.1, @@ -37,6 +38,7 @@ func newCamera() *Camera { minExposure: 0.00001, autoExposureSpeed: 2.0, exposure: 1.0, + cascadeDistances: []float32{16.0, 160.0, 1600.0}, } } @@ -44,6 +46,7 @@ func newCamera() *Camera { type Camera struct { Node + scene *Scene fov sprec.Angle fovMode FoVMode aspectRatio float32 @@ -57,6 +60,7 @@ type Camera struct { maxExposure float32 minExposure float32 exposure float32 + cascadeDistances []float32 } // FoV returns the field-of-view angle for this camera. @@ -196,6 +200,37 @@ func (c *Camera) SetExposure(exposure float32) { c.exposure = exposure } +// CascadeDistances returns the distances at which the shadow maps +// of this camera will be split into cascades. +func (c *Camera) CascadeDistances() []float32 { + return c.cascadeDistances +} + +// SetCascadeDistances changes the distances at which the shadow maps +// of this camera will be split into cascades. +func (c *Camera) SetCascadeDistances(distances []float32) { + c.cascadeDistances = distances +} + +// CascadeNear returns the near distance of the specified cascade index. +func (c *Camera) CascadeNear(index int) float32 { + if index <= 0 { + return c.near + } + return c.cascadeDistances[index-1] +} + +// CascadeFar returns the far distance of the specified cascade index. +func (c *Camera) CascadeFar(index int) float32 { + if index >= len(c.cascadeDistances) { + return c.far + } + return c.cascadeDistances[index] +} + // Delete removes this camera from the scene. func (c *Camera) Delete() { + if c.scene.activeCamera == c { + c.scene.SetActiveCamera(nil) + } } diff --git a/game/graphics/debug.go b/game/graphics/debug.go index ddf90e62..b1e8f356 100644 --- a/game/graphics/debug.go +++ b/game/graphics/debug.go @@ -15,7 +15,7 @@ func (d *Debug) Reset() { } func (d *Debug) Line(start, end, color dprec.Vec3) { - d.renderer.QueueDebugLine(debugLine{ + d.renderer.QueueDebugLine(DebugLine{ Start: dtos.Vec3(start), End: dtos.Vec3(end), Color: dtos.Vec3(color), @@ -28,7 +28,7 @@ func (d *Debug) Triangle(p1, p2, p3, color dprec.Vec3) { d.Line(p3, p1, color) } -type debugLine struct { +type DebugLine struct { Start sprec.Vec3 End sprec.Vec3 Color sprec.Vec3 diff --git a/game/graphics/engine.go b/game/graphics/engine.go index f815554b..806872f6 100644 --- a/game/graphics/engine.go +++ b/game/graphics/engine.go @@ -8,17 +8,38 @@ import ( "github.com/mokiat/lacking/render" ) -func NewEngine(api render.API, shaders ShaderCollection, shaderBuilder ShaderBuilder) *Engine { - stageData := newCommonStageData(api) - renderer := newRenderer(api, shaders, stageData) +func NewEngine(api render.API, shaders ShaderCollection, shaderBuilder ShaderBuilder, opts ...Option) *Engine { + cfg := &config{ + StageBuilder: DefaultStageBuilder, + + DirectionalShadowMapCount: 1, + DirectionalShadowMapSize: 2048, + DirectionalShadowMapCascadeCount: 4, + + SpotShadowMapCount: 3, + SpotShadowMapSize: 1024, + + PointShadowMapCount: 3, + PointShadowMapSize: 1024, + } + for _, opt := range opts { + opt(cfg) + } + + stageData := newCommonStageData(api, cfg) + meshRenderer := newMeshRenderer() + stageProvider := newStageProvider(api, shaders, stageData, meshRenderer) + stages := cfg.StageBuilder(stageProvider) + renderer := newRenderer(api, stageData, stages) return &Engine{ api: api, shaders: shaders, shaderBuilder: shaderBuilder, - stageData: stageData, - renderer: renderer, + stageData: stageData, + meshRenderer: meshRenderer, + renderer: renderer, debug: &Debug{ renderer: renderer, @@ -32,8 +53,9 @@ type Engine struct { shaders ShaderCollection shaderBuilder ShaderBuilder - stageData *commonStageData - renderer *sceneRenderer + stageData *commonStageData + meshRenderer *meshRenderer + renderer *sceneRenderer debug *Debug diff --git a/game/graphics/internal/shadow.go b/game/graphics/internal/shadow.go new file mode 100644 index 00000000..3a7a847b --- /dev/null +++ b/game/graphics/internal/shadow.go @@ -0,0 +1,32 @@ +package internal + +import ( + "github.com/mokiat/gomath/sprec" + "github.com/mokiat/lacking/render" +) + +type DirectionalShadowMap struct { + ArrayTexture render.Texture + Cascades []DirectionalShadowMapCascade +} + +type DirectionalShadowMapCascade struct { + Framebuffer render.Framebuffer + ProjectionMatrix sprec.Mat4 + Near float32 + Far float32 +} + +type DirectionalShadowMapRef struct { + DirectionalShadowMap +} + +type SpotShadowMap struct { + Texture render.Texture + Framebuffer render.Framebuffer +} + +type PointShadowMap struct { + ArrayTexture render.Texture + Framebuffers [6]render.Framebuffer +} diff --git a/game/graphics/internal/uniform.go b/game/graphics/internal/uniform.go index 0c3411c2..a6ab84bf 100644 --- a/game/graphics/internal/uniform.go +++ b/game/graphics/internal/uniform.go @@ -1,15 +1,11 @@ package internal const ( - UniformBufferBindingCamera = 0 - - UniformBufferBindingModel = 1 - UniformBufferBindingMaterial = 2 - UniformBufferBindingArmature = 3 - - UniformBufferBindingLight = 4 - UniformBufferBindingLightProperties = 5 - + UniformBufferBindingCamera = 0 + UniformBufferBindingModel = 1 + UniformBufferBindingMaterial = 2 + UniformBufferBindingArmature = 3 + UniformBufferBindingLight = 4 UniformBufferBindingPostprocess = 6 UniformBufferBindingBloom = 7 ) @@ -20,10 +16,11 @@ const ( TextureBindingLightingFramebufferColor0 = 0 TextureBindingLightingFramebufferColor1 = 1 TextureBindingLightingFramebufferDepth = 3 - TextureBindingShadowFramebufferDepth = 4 TextureBindingLightingReflectionTexture = 4 TextureBindingLightingRefractionTexture = 5 + TextureBindingLightingShadowMap = 4 + TextureBindingPostprocessFramebufferColor0 = 0 TextureBindingPostprocessBloom = 1 ) diff --git a/game/graphics/internal/uniform_lighting.go b/game/graphics/internal/uniform_lighting.go index 794063d7..561843c0 100644 --- a/game/graphics/internal/uniform_lighting.go +++ b/game/graphics/internal/uniform_lighting.go @@ -6,22 +6,11 @@ import ( ) type LightUniform struct { - ProjectionMatrix sprec.Mat4 - ViewMatrix sprec.Mat4 - LightMatrix sprec.Mat4 -} + ShadowMatrices [8]sprec.Mat4 + ModelMatrix sprec.Mat4 -func (u LightUniform) Std140Plot(plotter *blob.Plotter) { - plotter.PlotSPMat4(u.ProjectionMatrix) - plotter.PlotSPMat4(u.ViewMatrix) - plotter.PlotSPMat4(u.LightMatrix) -} + ShadowCascades [8]sprec.Vec2 -func (u LightUniform) Std140Size() uint32 { - return 64 + 64 + 64 -} - -type LightPropertiesUniform struct { Color sprec.Vec3 Intensity float32 @@ -30,7 +19,20 @@ type LightPropertiesUniform struct { InnerAngle float32 } -func (u LightPropertiesUniform) Std140Plot(plotter *blob.Plotter) { +func (u LightUniform) Std140Plot(plotter *blob.Plotter) { + // 8 x mat4 + for _, matrix := range u.ShadowMatrices { + plotter.PlotSPMat4(matrix) + } + // mat4 + plotter.PlotSPMat4(u.ModelMatrix) + + // 8 x vec4 + for _, cascade := range u.ShadowCascades { + plotter.PlotSPVec2(cascade) + plotter.Skip(2 * 4) + } + // vec4 plotter.PlotSPVec3(u.Color) plotter.PlotFloat32(u.Intensity) @@ -42,6 +44,6 @@ func (u LightPropertiesUniform) Std140Plot(plotter *blob.Plotter) { plotter.Skip(4) } -func (u LightPropertiesUniform) Std140Size() uint32 { - return 16 + 16 +func (u LightUniform) Std140Size() uint32 { + return 8*64 + 64 + 8*4*4 + 4*4 + 4*4 } diff --git a/game/graphics/ambient_light.go b/game/graphics/light_ambient.go similarity index 93% rename from game/graphics/ambient_light.go rename to game/graphics/light_ambient.go index 6e0aea43..87679c5d 100644 --- a/game/graphics/ambient_light.go +++ b/game/graphics/light_ambient.go @@ -37,6 +37,7 @@ type AmbientLight struct { scene *Scene itemID spatial.DynamicSetItemID + position dprec.Vec3 innerRadius float64 outerRadius float64 reflectionTexture render.Texture @@ -53,6 +54,10 @@ func (l *AmbientLight) SetActive(active bool) { l.active = active } +func (l *AmbientLight) Position() dprec.Vec3 { + return l.position +} + func (l *AmbientLight) Delete() { if l.scene == nil { panic("ambient light already deleted") diff --git a/game/graphics/directional_light.go b/game/graphics/light_directional.go similarity index 78% rename from game/graphics/directional_light.go rename to game/graphics/light_directional.go index 312dbdcc..ddde080c 100644 --- a/game/graphics/directional_light.go +++ b/game/graphics/light_directional.go @@ -7,12 +7,13 @@ import ( "github.com/mokiat/lacking/util/spatial" ) +const dirLightRadius = 16000.0 + type DirectionalLightInfo struct { Position dprec.Vec3 Rotation dprec.Quat EmitColor dprec.Vec3 - EmitRange float64 - CastShadow bool // TODO: Implement shadow casting + CastShadow bool } func newDirectionalLight(scene *Scene, info DirectionalLightInfo) *DirectionalLight { @@ -20,14 +21,14 @@ func newDirectionalLight(scene *Scene, info DirectionalLightInfo) *DirectionalLi light.scene = scene light.itemID = scene.directionalLightSet.Insert( - info.Position, info.EmitRange, light, + info.Position, dirLightRadius, light, ) light.active = true light.position = info.Position light.rotation = info.Rotation - light.emitRange = info.EmitRange light.emitColor = info.EmitColor + light.castShadow = info.CastShadow light.matrix = sprec.IdentityMat4() light.matrixDirty = true @@ -38,11 +39,11 @@ type DirectionalLight struct { scene *Scene itemID spatial.DynamicSetItemID - active bool - position dprec.Vec3 - rotation dprec.Quat - emitRange float64 - emitColor dprec.Vec3 + active bool + position dprec.Vec3 + rotation dprec.Quat + emitColor dprec.Vec3 + castShadow bool matrix sprec.Mat4 matrixDirty bool @@ -68,7 +69,7 @@ func (l *DirectionalLight) SetPosition(position dprec.Vec3) { if position != l.position { l.position = position l.scene.directionalLightSet.Update( - l.itemID, l.position, l.emitRange, + l.itemID, l.position, dirLightRadius, ) l.matrixDirty = true } @@ -87,21 +88,6 @@ func (l *DirectionalLight) SetRotation(rotation dprec.Quat) { } } -// EmitRange returns the distance that this light source covers. -func (l *DirectionalLight) EmitRange() float64 { - return l.emitRange -} - -// SetEmitRange changes the distance that this light source covers. -func (l *DirectionalLight) SetEmitRange(emitRange float64) { - if emitRange != l.emitRange { - l.emitRange = dprec.Max(0.0, emitRange) - l.scene.directionalLightSet.Update( - l.itemID, l.position, l.emitRange, - ) - } -} - // EmitColor returns the linear color of this light. func (l *DirectionalLight) EmitColor() dprec.Vec3 { return l.emitColor @@ -113,6 +99,16 @@ func (l *DirectionalLight) SetEmitColor(color dprec.Vec3) { l.emitColor = color } +// CastShadow returns whether this light will cast a shadow. +func (l *DirectionalLight) CastShadow() bool { + return l.castShadow +} + +// SetCastShadow changes whether this light will cast a shadow. +func (l *DirectionalLight) SetCastShadow(castShadow bool) { + l.castShadow = castShadow +} + // Delete removes this light from the scene. func (l *DirectionalLight) Delete() { if l.scene == nil { diff --git a/game/graphics/point_light.go b/game/graphics/light_point.go similarity index 100% rename from game/graphics/point_light.go rename to game/graphics/light_point.go diff --git a/game/graphics/spot_light.go b/game/graphics/light_spot.go similarity index 100% rename from game/graphics/spot_light.go rename to game/graphics/light_spot.go diff --git a/game/graphics/option.go b/game/graphics/option.go new file mode 100644 index 00000000..bc163a5d --- /dev/null +++ b/game/graphics/option.go @@ -0,0 +1,95 @@ +package graphics + +// Option is a configuration function that can be used to customize the +// behavior of the graphics engine. +type Option func(*config) + +// WithStageBuilder configures the graphics engine to use the specified stage +// builder function. +func WithStageBuilder(builder StageBuilderFunc) Option { + return func(c *config) { + c.StageBuilder = builder + } +} + +// WithDirectionalShadowMapCount configures the graphics engine to use the +// specified number of directional shadow maps. +// +// This value controls the number of directional lights that can have shadows +// at the same time. +func WithDirectionalShadowMapCount(count int) Option { + return func(c *config) { + c.DirectionalShadowMapCount = count + } +} + +// WithDirectionalShadowMapSize configures the graphics engine to use the +// specified size for the directional shadow maps. The size needs to be a power +// of two. +func WithDirectionalShadowMapSize(size int) Option { + return func(c *config) { + c.DirectionalShadowMapSize = size + } +} + +// WithDirectionalShadowMapCascadeCount configures the maximum number of +// cascades that the directional shadow maps will have. +// +// This value cannot be smaller than 1 and larger than 8 and will be clamped. +func WithDirectionalShadowMapCascadeCount(count int) Option { + return func(c *config) { + c.DirectionalShadowMapCascadeCount = max(1, min(count, 8)) + } +} + +// WithSpotShadowMapCount configures the graphics engine to use the specified +// number of spot light shadow maps. +// +// This value controls the number of spot lights that can have shadows at the +// same time. +func WithSpotShadowMapCount(count int) Option { + return func(c *config) { + c.SpotShadowMapCount = count + } +} + +// WithSpotShadowMapSize configures the graphics engine to use the specified +// size for the spot light shadow maps. The size needs to be a power of two. +func WithSpotShadowMapSize(size int) Option { + return func(c *config) { + c.SpotShadowMapSize = size + } +} + +// WithPointShadowMapCount configures the graphics engine to use the specified +// number of point light shadow maps. +// +// This value controls the number of point lights that can have shadows at the +// same time. +func WithPointShadowMapCount(count int) Option { + return func(c *config) { + c.PointShadowMapCount = count + } +} + +// WithPointShadowMapSize configures the graphics engine to use the specified +// size for the point light shadow maps. The size needs to be a power of two. +func WithPointShadowMapSize(size int) Option { + return func(c *config) { + c.PointShadowMapSize = size + } +} + +type config struct { + StageBuilder StageBuilderFunc + + DirectionalShadowMapCount int + DirectionalShadowMapSize int + DirectionalShadowMapCascadeCount int + + SpotShadowMapCount int + SpotShadowMapSize int + + PointShadowMapCount int + PointShadowMapSize int +} diff --git a/game/graphics/renderer.go b/game/graphics/renderer.go index b8f6eb48..bdf8f61a 100644 --- a/game/graphics/renderer.go +++ b/game/graphics/renderer.go @@ -1,534 +1,74 @@ package graphics import ( - "cmp" "fmt" - "math" - "slices" - "time" - "github.com/mokiat/gblob" - "github.com/mokiat/gog/ds" - "github.com/mokiat/gog/opt" "github.com/mokiat/gomath/dprec" - "github.com/mokiat/gomath/dtos" "github.com/mokiat/gomath/sprec" "github.com/mokiat/gomath/stod" "github.com/mokiat/lacking/debug/metric" "github.com/mokiat/lacking/game/graphics/internal" "github.com/mokiat/lacking/render" "github.com/mokiat/lacking/render/ubo" - "github.com/mokiat/lacking/util/blob" "github.com/mokiat/lacking/util/spatial" - "github.com/x448/float16" ) const ( - shadowMapWidth = 2048 - shadowMapHeight = 2048 - commandBufferSize = 2 * 1024 * 1024 // 2MB uniformBufferSize = 32 * 1024 * 1024 // 32MB - - // TODO: Move these next to the uniform types - modelUniformBufferItemSize = 64 - modelUniformBufferItemCount = 256 - modelUniformBufferSize = modelUniformBufferItemSize * modelUniformBufferItemCount ) -var ShowLightView bool - -func newRenderer(api render.API, shaders ShaderCollection, stageData *commonStageData) *sceneRenderer { +func newRenderer(api render.API, stageData *commonStageData, stages []Stage) *sceneRenderer { return &sceneRenderer{ - api: api, - shaders: shaders, - + api: api, stageData: stageData, + stages: stages, - exposureBufferData: make([]byte, 4*render.SizeF32), // Worst case RGBA32F - exposureTarget: 1.0, - - visibleStaticMeshes: spatial.NewVisitorBucket[uint32](2_000), - visibleMeshes: spatial.NewVisitorBucket[*Mesh](2_000), + debugLines: make([]DebugLine, debugMaxLineCount), - litStaticMeshes: spatial.NewVisitorBucket[uint32](2_000), - litMeshes: spatial.NewVisitorBucket[*Mesh](2_000), + visibleAmbientLights: spatial.NewVisitorBucket[*AmbientLight](1), + visiblePointLights: spatial.NewVisitorBucket[*PointLight](32), + visibleSpotLights: spatial.NewVisitorBucket[*SpotLight](8), + visibleDirectionalLights: spatial.NewVisitorBucket[*DirectionalLight](2), - bloomStage: newBloomRenderStage(api, shaders, stageData), - - ambientLightBucket: spatial.NewVisitorBucket[*AmbientLight](16), - - pointLightBucket: spatial.NewVisitorBucket[*PointLight](16), - - spotLightBucket: spatial.NewVisitorBucket[*SpotLight](16), - - directionalLightBucket: spatial.NewVisitorBucket[*DirectionalLight](16), + visibleStaticMeshes: spatial.NewVisitorBucket[uint32](65536), + visibleMeshes: spatial.NewVisitorBucket[*Mesh](1024), } } type sceneRenderer struct { - api render.API - shaders ShaderCollection - + api render.API stageData *commonStageData + stages []Stage - framebufferWidth uint32 - framebufferHeight uint32 - - nearestSampler render.Sampler - linearSampler render.Sampler - depthSampler render.Sampler - - geometryAlbedoTexture render.Texture - geometryNormalTexture render.Texture - geometryDepthTexture render.Texture - geometryFramebuffer render.Framebuffer - - lightingAlbedoTexture render.Texture - lightingFramebuffer render.Framebuffer - - forwardFramebuffer render.Framebuffer - - shadowDepthTexture render.Texture - shadowFramebuffer render.Framebuffer - - exposureAlbedoTexture render.Texture - exposureFramebuffer render.Framebuffer - exposureFormat render.DataFormat - exposureProgram render.Program - exposurePipeline render.Pipeline - exposureBufferData gblob.LittleEndianBlock - exposureBuffer render.Buffer - exposureSync render.Fence - exposureTarget float32 - exposureUpdateTimestamp time.Time - - bloomStage *bloomRenderStage - - postprocessingProgram render.Program - postprocessingPipeline render.Pipeline - - ambientLightProgram render.Program - ambientLightPipeline render.Pipeline - ambientLightBucket *spatial.VisitorBucket[*AmbientLight] - - pointLightProgram render.Program - pointLightPipeline render.Pipeline - pointLightBucket *spatial.VisitorBucket[*PointLight] + debugLines []DebugLine - spotLightProgram render.Program - spotLightPipeline render.Pipeline - spotLightBucket *spatial.VisitorBucket[*SpotLight] - - directionalLightProgram render.Program - directionalLightPipeline render.Pipeline - directionalLightBucket *spatial.VisitorBucket[*DirectionalLight] - - debugLines []debugLine - debugVertexData []byte - debugVertexBuffer render.Buffer - debugVertexArray render.VertexArray - debugProgram render.Program - debugPipeline render.Pipeline + visibleAmbientLights *spatial.VisitorBucket[*AmbientLight] + visiblePointLights *spatial.VisitorBucket[*PointLight] + visibleSpotLights *spatial.VisitorBucket[*SpotLight] + visibleDirectionalLights *spatial.VisitorBucket[*DirectionalLight] visibleStaticMeshes *spatial.VisitorBucket[uint32] visibleMeshes *spatial.VisitorBucket[*Mesh] - - litStaticMeshes *spatial.VisitorBucket[uint32] - litMeshes *spatial.VisitorBucket[*Mesh] - - renderItems []renderItem - - modelUniformBufferData gblob.LittleEndianBlock - cameraPlacement ubo.UniformPlacement -} - -func (r *sceneRenderer) createFramebuffers(width, height uint32) { - r.framebufferWidth = width - r.framebufferHeight = height - - r.geometryAlbedoTexture = r.api.CreateColorTexture2D(render.ColorTexture2DInfo{ - Width: r.framebufferWidth, - Height: r.framebufferHeight, - GenerateMipmaps: false, - GammaCorrection: false, - Format: render.DataFormatRGBA8, - }) - r.geometryNormalTexture = r.api.CreateColorTexture2D(render.ColorTexture2DInfo{ - Width: r.framebufferWidth, - Height: r.framebufferHeight, - GenerateMipmaps: false, - GammaCorrection: false, - Format: render.DataFormatRGBA16F, - }) - r.geometryDepthTexture = r.api.CreateDepthTexture2D(render.DepthTexture2DInfo{ - Width: r.framebufferWidth, - Height: r.framebufferHeight, - }) - r.geometryFramebuffer = r.api.CreateFramebuffer(render.FramebufferInfo{ - ColorAttachments: [4]render.Texture{ - r.geometryAlbedoTexture, - r.geometryNormalTexture, - }, - DepthAttachment: r.geometryDepthTexture, - }) - - r.lightingAlbedoTexture = r.api.CreateColorTexture2D(render.ColorTexture2DInfo{ - Width: r.framebufferWidth, - Height: r.framebufferHeight, - GenerateMipmaps: false, - GammaCorrection: false, - Format: render.DataFormatRGBA16F, - }) - r.lightingFramebuffer = r.api.CreateFramebuffer(render.FramebufferInfo{ - ColorAttachments: [4]render.Texture{ - r.lightingAlbedoTexture, - }, - }) - - r.forwardFramebuffer = r.api.CreateFramebuffer(render.FramebufferInfo{ - ColorAttachments: [4]render.Texture{ - r.lightingAlbedoTexture, - }, - DepthAttachment: r.geometryDepthTexture, - }) -} - -func (r *sceneRenderer) releaseFramebuffers() { - defer r.geometryAlbedoTexture.Release() - defer r.geometryNormalTexture.Release() - defer r.geometryDepthTexture.Release() - defer r.geometryFramebuffer.Release() - - defer r.lightingAlbedoTexture.Release() - defer r.lightingFramebuffer.Release() - - defer r.forwardFramebuffer.Release() } func (r *sceneRenderer) Allocate() { - r.createFramebuffers(800, 600) - r.bloomStage.Allocate() - - r.nearestSampler = r.api.CreateSampler(render.SamplerInfo{ - Wrapping: render.WrapModeClamp, - Filtering: render.FilterModeNearest, - Mipmapping: false, - }) - r.linearSampler = r.api.CreateSampler(render.SamplerInfo{ - Wrapping: render.WrapModeClamp, - Filtering: render.FilterModeLinear, - Mipmapping: false, - }) - r.depthSampler = r.api.CreateSampler(render.SamplerInfo{ - Wrapping: render.WrapModeClamp, - Filtering: render.FilterModeLinear, - Comparison: opt.V(render.ComparisonLess), - Mipmapping: false, - }) - - quadShape := r.stageData.QuadShape() - sphereShape := r.stageData.SphereShape() - coneShape := r.stageData.ConeShape() - - r.shadowDepthTexture = r.api.CreateDepthTexture2D(render.DepthTexture2DInfo{ - Width: shadowMapWidth, - Height: shadowMapHeight, - Comparable: true, - }) - r.shadowFramebuffer = r.api.CreateFramebuffer(render.FramebufferInfo{ - DepthAttachment: r.shadowDepthTexture, - }) - - r.exposureAlbedoTexture = r.api.CreateColorTexture2D(render.ColorTexture2DInfo{ - Width: 1, - Height: 1, - GenerateMipmaps: false, - GammaCorrection: false, - Format: render.DataFormatRGBA16F, - }) - r.exposureFramebuffer = r.api.CreateFramebuffer(render.FramebufferInfo{ - ColorAttachments: [4]render.Texture{ - r.exposureAlbedoTexture, - }, - }) - r.exposureProgram = r.api.CreateProgram(render.ProgramInfo{ - SourceCode: r.shaders.ExposureSet(), - TextureBindings: []render.TextureBinding{ - render.NewTextureBinding("fbColor0TextureIn", internal.TextureBindingLightingFramebufferColor0), - }, - UniformBindings: []render.UniformBinding{}, - }) - r.exposurePipeline = r.api.CreatePipeline(render.PipelineInfo{ - Program: r.exposureProgram, - VertexArray: quadShape.VertexArray(), - Topology: quadShape.Topology(), - Culling: render.CullModeBack, - FrontFace: render.FaceOrientationCCW, - DepthTest: false, - DepthWrite: false, - StencilTest: false, - ColorWrite: render.ColorMaskTrue, - BlendEnabled: false, - }) - r.exposureBuffer = r.api.CreatePixelTransferBuffer(render.BufferInfo{ - Dynamic: true, - Size: uint32(len(r.exposureBufferData)), - }) - r.exposureFormat = r.api.DetermineContentFormat(r.exposureFramebuffer) - if r.exposureFormat == render.DataFormatUnsupported { - // This happens on MacOS on native; fallback to a default format and - // hope for the best. - r.exposureFormat = render.DataFormatRGBA32F + for _, stage := range r.stages { + stage.Allocate() } - - r.postprocessingProgram = r.api.CreateProgram(render.ProgramInfo{ - SourceCode: r.shaders.PostprocessingSet(PostprocessingShaderConfig{ - ToneMapping: ExponentialToneMapping, - Bloom: true, - }), - TextureBindings: []render.TextureBinding{ - render.NewTextureBinding("fbColor0TextureIn", internal.TextureBindingPostprocessFramebufferColor0), - render.NewTextureBinding("lackingBloomTexture", internal.TextureBindingPostprocessBloom), - }, - UniformBindings: []render.UniformBinding{ - render.NewUniformBinding("Postprocess", internal.UniformBufferBindingPostprocess), - }, - }) - r.postprocessingPipeline = r.api.CreatePipeline(render.PipelineInfo{ - Program: r.postprocessingProgram, - VertexArray: quadShape.VertexArray(), - Topology: quadShape.Topology(), - Culling: render.CullModeBack, - FrontFace: render.FaceOrientationCCW, - DepthTest: false, - DepthWrite: false, - DepthComparison: render.ComparisonAlways, - StencilTest: false, - ColorWrite: [4]bool{true, true, true, true}, - BlendEnabled: false, - }) - - r.ambientLightProgram = r.api.CreateProgram(render.ProgramInfo{ - SourceCode: r.shaders.AmbientLightSet(), - TextureBindings: []render.TextureBinding{ - render.NewTextureBinding("fbColor0TextureIn", internal.TextureBindingLightingFramebufferColor0), - render.NewTextureBinding("fbColor1TextureIn", internal.TextureBindingLightingFramebufferColor1), - render.NewTextureBinding("fbDepthTextureIn", internal.TextureBindingLightingFramebufferDepth), - render.NewTextureBinding("reflectionTextureIn", internal.TextureBindingLightingReflectionTexture), - render.NewTextureBinding("refractionTextureIn", internal.TextureBindingLightingRefractionTexture), - }, - UniformBindings: []render.UniformBinding{ - render.NewUniformBinding("Camera", internal.UniformBufferBindingCamera), - }, - }) - r.ambientLightPipeline = r.api.CreatePipeline(render.PipelineInfo{ - Program: r.ambientLightProgram, - VertexArray: quadShape.VertexArray(), - Topology: quadShape.Topology(), - Culling: render.CullModeBack, - FrontFace: render.FaceOrientationCCW, - DepthTest: false, - DepthWrite: false, - DepthComparison: render.ComparisonAlways, - StencilTest: false, - ColorWrite: render.ColorMaskTrue, - BlendEnabled: true, - BlendColor: [4]float32{0.0, 0.0, 0.0, 0.0}, - BlendSourceColorFactor: render.BlendFactorOne, - BlendSourceAlphaFactor: render.BlendFactorOne, - BlendDestinationColorFactor: render.BlendFactorOne, - BlendDestinationAlphaFactor: render.BlendFactorZero, - BlendOpColor: render.BlendOperationAdd, - BlendOpAlpha: render.BlendOperationAdd, - }) - - r.pointLightProgram = r.api.CreateProgram(render.ProgramInfo{ - SourceCode: r.shaders.PointLightSet(), - TextureBindings: []render.TextureBinding{ - render.NewTextureBinding("fbColor0TextureIn", internal.TextureBindingLightingFramebufferColor0), - render.NewTextureBinding("fbColor1TextureIn", internal.TextureBindingLightingFramebufferColor1), - render.NewTextureBinding("fbDepthTextureIn", internal.TextureBindingLightingFramebufferDepth), - }, - UniformBindings: []render.UniformBinding{ - render.NewUniformBinding("Camera", internal.UniformBufferBindingCamera), - render.NewUniformBinding("Light", internal.UniformBufferBindingLight), - render.NewUniformBinding("LightProperties", internal.UniformBufferBindingLightProperties), - }, - }) - r.pointLightPipeline = r.api.CreatePipeline(render.PipelineInfo{ - Program: r.pointLightProgram, - VertexArray: sphereShape.VertexArray(), - Topology: sphereShape.Topology(), - Culling: render.CullModeFront, - FrontFace: render.FaceOrientationCCW, - DepthTest: false, - DepthWrite: false, - DepthComparison: render.ComparisonAlways, - StencilTest: false, - ColorWrite: render.ColorMaskTrue, - BlendEnabled: true, - BlendColor: [4]float32{0.0, 0.0, 0.0, 0.0}, - BlendSourceColorFactor: render.BlendFactorOne, - BlendSourceAlphaFactor: render.BlendFactorOne, - BlendDestinationColorFactor: render.BlendFactorOne, - BlendDestinationAlphaFactor: render.BlendFactorZero, - BlendOpColor: render.BlendOperationAdd, - BlendOpAlpha: render.BlendOperationAdd, - }) - - r.spotLightProgram = r.api.CreateProgram(render.ProgramInfo{ - SourceCode: r.shaders.SpotLightSet(), - TextureBindings: []render.TextureBinding{ - render.NewTextureBinding("fbColor0TextureIn", internal.TextureBindingLightingFramebufferColor0), - render.NewTextureBinding("fbColor1TextureIn", internal.TextureBindingLightingFramebufferColor1), - render.NewTextureBinding("fbDepthTextureIn", internal.TextureBindingLightingFramebufferDepth), - }, - UniformBindings: []render.UniformBinding{ - render.NewUniformBinding("Camera", internal.UniformBufferBindingCamera), - render.NewUniformBinding("Light", internal.UniformBufferBindingLight), - render.NewUniformBinding("LightProperties", internal.UniformBufferBindingLightProperties), - }, - }) - r.spotLightPipeline = r.api.CreatePipeline(render.PipelineInfo{ - Program: r.spotLightProgram, - VertexArray: coneShape.VertexArray(), - Topology: coneShape.Topology(), - Culling: render.CullModeFront, - FrontFace: render.FaceOrientationCCW, - DepthTest: false, - DepthWrite: false, - DepthComparison: render.ComparisonAlways, - StencilTest: false, - ColorWrite: render.ColorMaskTrue, - BlendEnabled: true, - BlendColor: [4]float32{0.0, 0.0, 0.0, 0.0}, - BlendSourceColorFactor: render.BlendFactorOne, - BlendSourceAlphaFactor: render.BlendFactorOne, - BlendDestinationColorFactor: render.BlendFactorOne, - BlendDestinationAlphaFactor: render.BlendFactorZero, - BlendOpColor: render.BlendOperationAdd, - BlendOpAlpha: render.BlendOperationAdd, - }) - - r.directionalLightProgram = r.api.CreateProgram(render.ProgramInfo{ - SourceCode: r.shaders.DirectionalLightSet(), - TextureBindings: []render.TextureBinding{ - render.NewTextureBinding("fbColor0TextureIn", internal.TextureBindingLightingFramebufferColor0), - render.NewTextureBinding("fbColor1TextureIn", internal.TextureBindingLightingFramebufferColor1), - render.NewTextureBinding("fbDepthTextureIn", internal.TextureBindingLightingFramebufferDepth), - render.NewTextureBinding("fbShadowTextureIn", internal.TextureBindingShadowFramebufferDepth), - }, - UniformBindings: []render.UniformBinding{ - render.NewUniformBinding("Camera", internal.UniformBufferBindingCamera), - render.NewUniformBinding("Light", internal.UniformBufferBindingLight), - render.NewUniformBinding("LightProperties", internal.UniformBufferBindingLightProperties), - }, - }) - r.directionalLightPipeline = r.api.CreatePipeline(render.PipelineInfo{ - Program: r.directionalLightProgram, - VertexArray: quadShape.VertexArray(), - Topology: quadShape.Topology(), - Culling: render.CullModeBack, - FrontFace: render.FaceOrientationCCW, - DepthTest: false, - DepthWrite: false, - DepthComparison: render.ComparisonAlways, - StencilTest: false, - ColorWrite: render.ColorMaskTrue, - BlendEnabled: true, - BlendColor: [4]float32{0.0, 0.0, 0.0, 0.0}, - BlendSourceColorFactor: render.BlendFactorOne, - BlendSourceAlphaFactor: render.BlendFactorOne, - BlendDestinationColorFactor: render.BlendFactorOne, - BlendDestinationAlphaFactor: render.BlendFactorZero, - BlendOpColor: render.BlendOperationAdd, - BlendOpAlpha: render.BlendOperationAdd, - }) - - r.debugLines = make([]debugLine, 131072) - r.debugVertexData = make([]byte, len(r.debugLines)*4*4*2) - r.debugVertexBuffer = r.api.CreateVertexBuffer(render.BufferInfo{ - Dynamic: true, - Data: r.debugVertexData, - }) - r.debugVertexArray = r.api.CreateVertexArray(render.VertexArrayInfo{ - Bindings: []render.VertexArrayBinding{ - render.NewVertexArrayBinding(r.debugVertexBuffer, 4*4), - }, - Attributes: []render.VertexArrayAttribute{ - render.NewVertexArrayAttribute(0, internal.CoordAttributeIndex, 0, render.VertexAttributeFormatRGB32F), - render.NewVertexArrayAttribute(0, internal.ColorAttributeIndex, 3*4, render.VertexAttributeFormatRGB8UN), - }, - }) - r.debugProgram = r.api.CreateProgram(render.ProgramInfo{ - SourceCode: r.shaders.DebugSet(), - TextureBindings: []render.TextureBinding{}, - UniformBindings: []render.UniformBinding{ - render.NewUniformBinding("Camera", internal.UniformBufferBindingCamera), - }, - }) - r.debugPipeline = r.api.CreatePipeline(render.PipelineInfo{ - Program: r.debugProgram, - VertexArray: r.debugVertexArray, - Topology: render.TopologyLineList, - Culling: render.CullModeNone, - FrontFace: render.FaceOrientationCCW, - DepthTest: true, - DepthWrite: false, - DepthComparison: render.ComparisonLessOrEqual, - StencilTest: false, - ColorWrite: render.ColorMaskTrue, - BlendEnabled: false, - }) - - r.modelUniformBufferData = make([]byte, modelUniformBufferSize) } func (r *sceneRenderer) Release() { - defer r.releaseFramebuffers() - - defer r.bloomStage.Release() - - defer r.nearestSampler.Release() - defer r.linearSampler.Release() - defer r.depthSampler.Release() - - defer r.shadowDepthTexture.Release() - defer r.shadowFramebuffer.Release() - - defer r.exposureAlbedoTexture.Release() - defer r.exposureFramebuffer.Release() - defer r.exposureProgram.Release() - defer r.exposurePipeline.Release() - defer r.exposureBuffer.Release() - - defer r.postprocessingProgram.Release() - defer r.postprocessingPipeline.Release() - - defer r.ambientLightProgram.Release() - defer r.ambientLightPipeline.Release() - - defer r.pointLightProgram.Release() - defer r.pointLightPipeline.Release() - - defer r.spotLightProgram.Release() - defer r.spotLightPipeline.Release() - - defer r.directionalLightProgram.Release() - defer r.directionalLightPipeline.Release() - - defer r.debugVertexBuffer.Release() - defer r.debugVertexArray.Release() - defer r.debugProgram.Release() - defer r.debugPipeline.Release() + for _, stage := range r.stages { + defer stage.Release() + } } func (r *sceneRenderer) ResetDebugLines() { r.debugLines = r.debugLines[:0] } -func (r *sceneRenderer) QueueDebugLine(line debugLine) { +func (r *sceneRenderer) QueueDebugLine(line DebugLine) { if len(r.debugLines) == cap(r.debugLines)-1 { logger.Warn("Debug lines limit reached!") } @@ -539,7 +79,7 @@ func (r *sceneRenderer) QueueDebugLine(line debugLine) { } func (r *sceneRenderer) Ray(viewport Viewport, camera *Camera, x, y int) (dprec.Vec3, dprec.Vec3) { - projectionMatrix := stod.Mat4(r.evaluateProjectionMatrix(camera, viewport.Width, viewport.Height)) + projectionMatrix := stod.Mat4(evaluateProjectionMatrix(camera, viewport.Width, viewport.Height)) inverseProjection := dprec.InverseMat4(projectionMatrix) cameraMatrix := stod.Mat4(camera.gfxMatrix()) @@ -564,7 +104,7 @@ func (r *sceneRenderer) Ray(viewport Viewport, camera *Camera, x, y int) (dprec. func (r *sceneRenderer) Point(viewport Viewport, camera *Camera, position dprec.Vec3) dprec.Vec2 { pos := dprec.NewVec4(position.X, position.Y, position.Z, 1.0) - projectionMatrix := stod.Mat4(r.evaluateProjectionMatrix(camera, viewport.Width, viewport.Height)) + projectionMatrix := stod.Mat4(evaluateProjectionMatrix(camera, viewport.Width, viewport.Height)) viewMatrix := stod.Mat4(sprec.InverseMat4(camera.gfxMatrix())) ndc := dprec.Mat4Vec4Prod(projectionMatrix, dprec.Mat4Vec4Prod(viewMatrix, pos)) if dprec.Abs(ndc.W) < 0.0001 { @@ -579,58 +119,17 @@ func (r *sceneRenderer) Render(framebuffer render.Framebuffer, viewport Viewport uniformBuffer := r.stageData.UniformBuffer() uniformBuffer.Reset() - if viewport.Width != r.framebufferWidth || viewport.Height != r.framebufferHeight { - r.releaseFramebuffers() - r.createFramebuffers(viewport.Width, viewport.Height) - r.bloomStage.Resize(viewport.Width, viewport.Height) + for _, stage := range r.stages { + stage.PreRender(viewport.Width, viewport.Height) } - projectionMatrix := r.evaluateProjectionMatrix(camera, viewport.Width, viewport.Height) + projectionMatrix := evaluateProjectionMatrix(camera, viewport.Width, viewport.Height) cameraMatrix := camera.gfxMatrix() viewMatrix := sprec.InverseMat4(cameraMatrix) projectionViewMatrix := sprec.Mat4Prod(projectionMatrix, viewMatrix) frustum := spatial.ProjectionRegion(stod.Mat4(projectionViewMatrix)) - if ShowLightView { - r.directionalLightBucket.Reset() - scene.directionalLightSet.VisitHexahedronRegion(&frustum, r.directionalLightBucket) - - var directionalLight *DirectionalLight - for _, light := range r.directionalLightBucket.Items() { - if light.active { - directionalLight = light - break - } - } - if directionalLight != nil { - projectionMatrix = lightOrtho() - cameraMatrix = directionalLight.gfxMatrix() - viewMatrix = sprec.InverseMat4(cameraMatrix) - projectionViewMatrix = sprec.Mat4Prod(projectionMatrix, viewMatrix) - frustum = spatial.ProjectionRegion(stod.Mat4(projectionViewMatrix)) - } - } - - ctx := renderCtx{ - framebuffer: framebuffer, - scene: scene, - x: viewport.X, - y: viewport.Y, - width: viewport.Width, - height: viewport.Height, - camera: camera, - cameraPosition: stod.Vec3(cameraMatrix.Translation()), - frustum: frustum, - time: scene.Time(), - } - - r.visibleMeshes.Reset() - ctx.scene.dynamicMeshSet.VisitHexahedronRegion(&frustum, r.visibleMeshes) - - r.visibleStaticMeshes.Reset() - ctx.scene.staticMeshOctree.VisitHexahedronRegion(&ctx.frustum, r.visibleStaticMeshes) - - r.cameraPlacement = ubo.WriteUniform(uniformBuffer, internal.CameraUniform{ + cameraPlacement := ubo.WriteUniform(uniformBuffer, internal.CameraUniform{ ProjectionMatrix: projectionMatrix, ViewMatrix: viewMatrix, CameraMatrix: cameraMatrix, @@ -640,18 +139,48 @@ func (r *sceneRenderer) Render(framebuffer render.Framebuffer, viewport Viewport float32(viewport.Width), float32(viewport.Height), ), - Time: ctx.time, + Time: scene.Time(), }) - r.renderShadowPass(ctx) - r.renderGeometryPass(ctx) - r.renderLightingPass(ctx) - r.renderForwardPass(ctx) - if camera.autoExposureEnabled { - r.renderExposureProbePass(ctx) + r.visibleAmbientLights.Reset() + scene.ambientLightSet.VisitHexahedronRegion(&frustum, r.visibleAmbientLights) + + r.visiblePointLights.Reset() + scene.pointLightSet.VisitHexahedronRegion(&frustum, r.visiblePointLights) + + r.visibleSpotLights.Reset() + scene.spotLightSet.VisitHexahedronRegion(&frustum, r.visibleSpotLights) + + r.visibleDirectionalLights.Reset() + scene.directionalLightSet.VisitHexahedronRegion(&frustum, r.visibleDirectionalLights) + + r.visibleMeshes.Reset() + scene.dynamicMeshSet.VisitHexahedronRegion(&frustum, r.visibleMeshes) + + r.visibleStaticMeshes.Reset() + scene.staticMeshOctree.VisitHexahedronRegion(&frustum, r.visibleStaticMeshes) + + stageCtx := StageContext{ + Scene: scene, + Camera: camera, + CameraPosition: stod.Vec3(cameraMatrix.Translation()), + CameraPlacement: cameraPlacement, + CameraFrustum: frustum, + VisibleAmbientLights: r.visibleAmbientLights.Items(), + VisiblePointLights: r.visiblePointLights.Items(), + VisibleSpotLights: r.visibleSpotLights.Items(), + VisibleDirectionalLights: r.visibleDirectionalLights.Items(), + VisibleMeshes: r.visibleMeshes.Items(), + VisibleStaticMeshIndices: r.visibleStaticMeshes.Items(), + DebugLines: r.debugLines, + Viewport: render.Area(viewport), + Framebuffer: framebuffer, + CommandBuffer: commandBuffer, + UniformBuffer: uniformBuffer, + } + for _, stage := range r.stages { + stage.Render(stageCtx) } - r.renderBloomStage() - r.renderPostprocessingPass(ctx) uniformSpan := metric.BeginRegion("upload") uniformBuffer.Upload() @@ -662,17 +191,17 @@ func (r *sceneRenderer) Render(framebuffer render.Framebuffer, viewport Viewport r.api.Queue().Submit(commandBuffer) submitSpan.End() - if camera.autoExposureEnabled && r.exposureSync == nil { - r.exposureSync = r.api.Queue().TrackSubmittedWorkDone() + for _, stage := range r.stages { + stage.PostRender() } } -func (r *sceneRenderer) evaluateProjectionMatrix(camera *Camera, width, height uint32) sprec.Mat4 { +func evaluateProjectionMatrix(camera *Camera, width, height uint32) sprec.Mat4 { var ( near = camera.Near() far = camera.Far() - fWidth = sprec.Max(1.0, float32(width)) - fHeight = sprec.Max(1.0, float32(height)) + fWidth = max(1.0, float32(width)) + fHeight = max(1.0, float32(height)) ) switch camera.fovMode { @@ -702,805 +231,6 @@ func (r *sceneRenderer) evaluateProjectionMatrix(camera *Camera, width, height u } } -func lightOrtho() sprec.Mat4 { - return sprec.OrthoMat4(-32, 32, 32, -32, 0, 256) -} - -func (r *sceneRenderer) renderShadowPass(ctx renderCtx) { - defer metric.BeginRegion("shadow").End() - - r.directionalLightBucket.Reset() - ctx.scene.directionalLightSet.VisitHexahedronRegion(&ctx.frustum, r.directionalLightBucket) - - var directionalLight *DirectionalLight - for _, light := range r.directionalLightBucket.Items() { - if light.active { - directionalLight = light - break - } - } - if directionalLight == nil { - return - } - - projectionMatrix := lightOrtho() - lightMatrix := directionalLight.gfxMatrix() - lightMatrix.M14 = sprec.Floor(lightMatrix.M14*shadowMapWidth) / float32(shadowMapWidth) - lightMatrix.M24 = sprec.Floor(lightMatrix.M24*shadowMapWidth) / float32(shadowMapWidth) - lightMatrix.M34 = sprec.Floor(lightMatrix.M34*shadowMapWidth) / float32(shadowMapWidth) - viewMatrix := sprec.InverseMat4(lightMatrix) - projectionViewMatrix := sprec.Mat4Prod(projectionMatrix, viewMatrix) - frustum := spatial.ProjectionRegion(stod.Mat4(projectionViewMatrix)) - - r.litMeshes.Reset() - ctx.scene.dynamicMeshSet.VisitHexahedronRegion(&frustum, r.litMeshes) - - r.litStaticMeshes.Reset() - ctx.scene.staticMeshOctree.VisitHexahedronRegion(&frustum, r.litStaticMeshes) - - r.renderItems = r.renderItems[:0] - for _, mesh := range r.litMeshes.Items() { - r.queueMeshRenderItems(mesh, internal.MeshRenderPassTypeShadow) - } - for _, meshIndex := range r.litStaticMeshes.Items() { - staticMesh := &ctx.scene.staticMeshes[meshIndex] - r.queueStaticMeshRenderItems(ctx, staticMesh, internal.MeshRenderPassTypeShadow) - } - - commandBuffer := r.stageData.CommandBuffer() - commandBuffer.BeginRenderPass(render.RenderPassInfo{ - Framebuffer: r.shadowFramebuffer, - Viewport: render.Area{ - X: 0, - Y: 0, - Width: shadowMapWidth, - Height: shadowMapHeight, - }, - DepthLoadOp: render.LoadOperationClear, - DepthStoreOp: render.StoreOperationStore, - DepthClearValue: 1.0, - StencilLoadOp: render.LoadOperationLoad, - StencilStoreOp: render.StoreOperationDiscard, - }) - - uniformBuffer := r.stageData.UniformBuffer() - lightCameraPlacement := ubo.WriteUniform(uniformBuffer, internal.CameraUniform{ - ProjectionMatrix: projectionMatrix, - ViewMatrix: viewMatrix, - CameraMatrix: lightMatrix, - Viewport: sprec.ZeroVec4(), // TODO? - Time: ctx.time, - }) - - meshCtx := renderMeshContext{ - CameraPlacement: lightCameraPlacement, - } - r.renderMeshRenderItems(meshCtx, r.renderItems) - commandBuffer.EndRenderPass() -} - -func (r *sceneRenderer) renderGeometryPass(ctx renderCtx) { - defer metric.BeginRegion("geometry").End() - - r.renderItems = r.renderItems[:0] - for _, mesh := range r.visibleMeshes.Items() { - r.queueMeshRenderItems(mesh, internal.MeshRenderPassTypeGeometry) - } - for _, meshIndex := range r.visibleStaticMeshes.Items() { - staticMesh := &ctx.scene.staticMeshes[meshIndex] - r.queueStaticMeshRenderItems(ctx, staticMesh, internal.MeshRenderPassTypeGeometry) - } - - commandBuffer := r.stageData.CommandBuffer() - commandBuffer.BeginRenderPass(render.RenderPassInfo{ - Framebuffer: r.geometryFramebuffer, - Viewport: render.Area{ - X: 0, - Y: 0, - Width: r.framebufferWidth, - Height: r.framebufferHeight, - }, - DepthLoadOp: render.LoadOperationClear, - DepthStoreOp: render.StoreOperationStore, - DepthClearValue: 1.0, - StencilLoadOp: render.LoadOperationLoad, - StencilStoreOp: render.StoreOperationDiscard, - Colors: [4]render.ColorAttachmentInfo{ - { - LoadOp: render.LoadOperationClear, - StoreOp: render.StoreOperationStore, - ClearValue: [4]float32{0.0, 0.0, 0.0, 1.0}, - }, - { - LoadOp: render.LoadOperationClear, - StoreOp: render.StoreOperationStore, - ClearValue: [4]float32{0.0, 0.0, 1.0, 0.0}, - }, - { - LoadOp: render.LoadOperationClear, - StoreOp: render.StoreOperationStore, - ClearValue: [4]float32{0.0, 0.0, 0.0, 1.0}, - }, - }, - }) - meshCtx := renderMeshContext{ - CameraPlacement: r.cameraPlacement, - } - r.renderMeshRenderItems(meshCtx, r.renderItems) - commandBuffer.EndRenderPass() -} - -func (r *sceneRenderer) renderLightingPass(ctx renderCtx) { - defer metric.BeginRegion("lighting").End() - - r.ambientLightBucket.Reset() - ctx.scene.ambientLightSet.VisitHexahedronRegion(&ctx.frustum, r.ambientLightBucket) - - r.pointLightBucket.Reset() - ctx.scene.pointLightSet.VisitHexahedronRegion(&ctx.frustum, r.pointLightBucket) - - r.spotLightBucket.Reset() - ctx.scene.spotLightSet.VisitHexahedronRegion(&ctx.frustum, r.spotLightBucket) - - r.directionalLightBucket.Reset() - ctx.scene.directionalLightSet.VisitHexahedronRegion(&ctx.frustum, r.directionalLightBucket) - - commandBuffer := r.stageData.CommandBuffer() - commandBuffer.BeginRenderPass(render.RenderPassInfo{ - Framebuffer: r.lightingFramebuffer, - Viewport: render.Area{ - X: 0, - Y: 0, - Width: r.framebufferWidth, - Height: r.framebufferHeight, - }, - DepthLoadOp: render.LoadOperationLoad, - DepthStoreOp: render.StoreOperationStore, - StencilLoadOp: render.LoadOperationLoad, - StencilStoreOp: render.StoreOperationDiscard, - Colors: [4]render.ColorAttachmentInfo{ - { - LoadOp: render.LoadOperationClear, - StoreOp: render.StoreOperationStore, - ClearValue: [4]float32{0.0, 0.0, 0.0, 1.0}, - }, - }, - }) - - // TODO: Use batching (instancing) when rendering lights, if possible. - - for _, ambientLight := range r.ambientLightBucket.Items() { - if ambientLight.active { - r.renderAmbientLight(ambientLight) - } - } - for _, pointLight := range r.pointLightBucket.Items() { - if pointLight.active { - r.renderPointLight(pointLight) - } - } - for _, spotLight := range r.spotLightBucket.Items() { - if spotLight.active { - r.renderSpotLight(spotLight) - } - } - for _, directionalLight := range r.directionalLightBucket.Items() { - if directionalLight.active { - r.renderDirectionalLight(directionalLight) - } - } - - commandBuffer.EndRenderPass() -} - -func (r *sceneRenderer) renderForwardPass(ctx renderCtx) { - defer metric.BeginRegion("forward").End() - - r.renderItems = r.renderItems[:0] - for _, mesh := range r.visibleMeshes.Items() { - r.queueMeshRenderItems(mesh, internal.MeshRenderPassTypeForward) - } - for _, meshIndex := range r.visibleStaticMeshes.Items() { - staticMesh := &ctx.scene.staticMeshes[meshIndex] - r.queueStaticMeshRenderItems(ctx, staticMesh, internal.MeshRenderPassTypeForward) - } - - commandBuffer := r.stageData.CommandBuffer() - commandBuffer.BeginRenderPass(render.RenderPassInfo{ - Framebuffer: r.forwardFramebuffer, - Viewport: render.Area{ - X: 0, - Y: 0, - Width: r.framebufferWidth, - Height: r.framebufferHeight, - }, - DepthLoadOp: render.LoadOperationLoad, - DepthStoreOp: render.StoreOperationStore, - StencilLoadOp: render.LoadOperationLoad, - StencilStoreOp: render.StoreOperationDiscard, - Colors: [4]render.ColorAttachmentInfo{ - { - LoadOp: render.LoadOperationLoad, - StoreOp: render.StoreOperationStore, - }, - }, - }) - - if sky := r.findActiveSky(ctx.scene.skies); sky != nil { - r.renderSky(sky) - } - - if len(r.debugLines) > 0 { - plotter := blob.NewPlotter(r.debugVertexData) - for _, line := range r.debugLines { - plotter.PlotSPVec3(line.Start) - plotter.PlotUint8(uint8(line.Color.X * 255)) - plotter.PlotUint8(uint8(line.Color.Y * 255)) - plotter.PlotUint8(uint8(line.Color.Z * 255)) - plotter.PlotUint8(uint8(255)) - - plotter.PlotSPVec3(line.End) - plotter.PlotUint8(uint8(line.Color.X * 255)) - plotter.PlotUint8(uint8(line.Color.Y * 255)) - plotter.PlotUint8(uint8(line.Color.Z * 255)) - plotter.PlotUint8(uint8(255)) - } - r.api.Queue().WriteBuffer(r.debugVertexBuffer, 0, r.debugVertexData[:plotter.Offset()]) - commandBuffer.BindPipeline(r.debugPipeline) - commandBuffer.UniformBufferUnit( - internal.UniformBufferBindingCamera, - r.cameraPlacement.Buffer, - r.cameraPlacement.Offset, - r.cameraPlacement.Size, - ) - commandBuffer.Draw(0, uint32(len(r.debugLines)*2), 1) - } - - // FIXME/TODO: Reusing renderItems and assuming same as geometry pass. - // Maybe rename the variable to something dedicated so that mistakes - // don't happen if ordering is changed in the future. - meshCtx := renderMeshContext{ - CameraPlacement: r.cameraPlacement, - } - r.renderMeshRenderItems(meshCtx, r.renderItems) - commandBuffer.EndRenderPass() -} - -func (r *sceneRenderer) findActiveSky(skies *ds.List[*Sky]) *Sky { - for _, sky := range skies.Unbox() { - if sky.Active() { - return sky - } - } - return nil -} - -func (r *sceneRenderer) renderSky(sky *Sky) { - commandBuffer := r.stageData.CommandBuffer() - uniformBuffer := r.stageData.UniformBuffer() - - for _, pass := range sky.definition.renderPasses { - commandBuffer.BindPipeline(pass.Pipeline) - commandBuffer.UniformBufferUnit( - internal.UniformBufferBindingCamera, - r.cameraPlacement.Buffer, - r.cameraPlacement.Offset, - r.cameraPlacement.Size, - ) - if !pass.UniformSet.IsEmpty() { - materialData := ubo.WriteUniform(uniformBuffer, internal.MaterialUniform{ - Data: pass.UniformSet.Data(), - }) - commandBuffer.UniformBufferUnit( - internal.UniformBufferBindingMaterial, - materialData.Buffer, - materialData.Offset, - materialData.Size, - ) - } - for i := range pass.TextureSet.TextureCount() { - if texture := pass.TextureSet.TextureAt(i); texture != nil { - commandBuffer.TextureUnit(uint(i), texture) - } - if sampler := pass.TextureSet.SamplerAt(i); sampler != nil { - commandBuffer.SamplerUnit(uint(i), sampler) - } - } - commandBuffer.DrawIndexed(pass.IndexByteOffset, pass.IndexCount, 1) - } -} - -func (r *sceneRenderer) renderExposureProbePass(ctx renderCtx) { - defer metric.BeginRegion("exposure").End() - - if r.exposureSync != nil { - switch r.exposureSync.Status() { - case render.FenceStatusSuccess: - r.api.Queue().ReadBuffer(r.exposureBuffer, 0, r.exposureBufferData) - var brightness float32 - switch r.exposureFormat { - case render.DataFormatRGBA16F: - brightness = float16.Frombits(r.exposureBufferData.Uint16(0)).Float32() - case render.DataFormatRGBA32F: - brightness = r.exposureBufferData.Float32(0) - } - brightness = sprec.Clamp(brightness, 0.001, 1000.0) - - r.exposureTarget = 1.0 / (2 * 3.14 * brightness) - if r.exposureTarget > ctx.camera.maxExposure { - r.exposureTarget = ctx.camera.maxExposure - } - if r.exposureTarget < ctx.camera.minExposure { - r.exposureTarget = ctx.camera.minExposure - } - r.exposureSync.Release() - r.exposureSync = nil - - case render.FenceStatusNotReady: - // wait until next frame - } - } - - if !r.exposureUpdateTimestamp.IsZero() { - elapsedSeconds := float32(time.Since(r.exposureUpdateTimestamp).Seconds()) - ctx.camera.exposure = sprec.Mix( - ctx.camera.exposure, - r.exposureTarget, - sprec.Clamp(ctx.camera.autoExposureSpeed*elapsedSeconds, 0.0, 1.0), - ) - } - r.exposureUpdateTimestamp = time.Now() - - if r.exposureSync == nil { - quadShape := r.stageData.QuadShape() - commandBuffer := r.stageData.CommandBuffer() - commandBuffer.BeginRenderPass(render.RenderPassInfo{ - Framebuffer: r.exposureFramebuffer, - Viewport: render.Area{ - X: 0, - Y: 0, - Width: 1, - Height: 1, - }, - DepthLoadOp: render.LoadOperationLoad, - DepthStoreOp: render.StoreOperationDiscard, - StencilLoadOp: render.LoadOperationLoad, - StencilStoreOp: render.StoreOperationDiscard, - Colors: [4]render.ColorAttachmentInfo{ - { - LoadOp: render.LoadOperationClear, - StoreOp: render.StoreOperationDiscard, - ClearValue: [4]float32{0.0, 0.0, 0.0, 0.0}, - }, - }, - }) - commandBuffer.BindPipeline(r.exposurePipeline) - commandBuffer.TextureUnit(internal.TextureBindingLightingFramebufferColor0, r.lightingAlbedoTexture) - commandBuffer.SamplerUnit(internal.TextureBindingLightingFramebufferColor0, r.nearestSampler) - commandBuffer.UniformBufferUnit( - internal.UniformBufferBindingCamera, - r.cameraPlacement.Buffer, - r.cameraPlacement.Offset, - r.cameraPlacement.Size, - ) - commandBuffer.DrawIndexed(0, quadShape.IndexCount(), 1) - commandBuffer.CopyFramebufferToBuffer(render.CopyFramebufferToBufferInfo{ - Buffer: r.exposureBuffer, - X: 0, - Y: 0, - Width: 1, - Height: 1, - Format: r.exposureFormat, - }) - commandBuffer.EndRenderPass() - } -} - -func (r *sceneRenderer) renderBloomStage() { - defer metric.BeginRegion("bloom").End() - r.bloomStage.Run(r.lightingAlbedoTexture) -} - -func (r *sceneRenderer) renderPostprocessingPass(ctx renderCtx) { - defer metric.BeginRegion("post").End() - - quadShape := r.stageData.QuadShape() - - uniformBuffer := r.stageData.UniformBuffer() - postprocessPlacement := ubo.WriteUniform(uniformBuffer, internal.PostprocessUniform{ - Exposure: ctx.camera.exposure, - }) - - commandBuffer := r.stageData.CommandBuffer() - commandBuffer.BeginRenderPass(render.RenderPassInfo{ - Framebuffer: ctx.framebuffer, - Viewport: render.Area{ - X: ctx.x, - Y: ctx.y, - Width: ctx.width, - Height: ctx.height, - }, - DepthLoadOp: render.LoadOperationLoad, - DepthStoreOp: render.StoreOperationDiscard, - StencilLoadOp: render.LoadOperationLoad, - StencilStoreOp: render.StoreOperationDiscard, - Colors: [4]render.ColorAttachmentInfo{ - { - LoadOp: render.LoadOperationLoad, - StoreOp: render.StoreOperationStore, - }, - }, - }) - - commandBuffer.BindPipeline(r.postprocessingPipeline) - commandBuffer.TextureUnit(internal.TextureBindingPostprocessFramebufferColor0, r.lightingAlbedoTexture) - commandBuffer.SamplerUnit(internal.TextureBindingPostprocessFramebufferColor0, r.nearestSampler) - commandBuffer.TextureUnit(internal.TextureBindingPostprocessBloom, r.bloomStage.OutputTexture()) - commandBuffer.SamplerUnit(internal.TextureBindingPostprocessBloom, r.bloomStage.OutputSampler()) - commandBuffer.UniformBufferUnit( - internal.UniformBufferBindingPostprocess, - postprocessPlacement.Buffer, - postprocessPlacement.Offset, - postprocessPlacement.Size, - ) - commandBuffer.DrawIndexed(0, quadShape.IndexCount(), 1) - - commandBuffer.EndRenderPass() -} - -func (r *sceneRenderer) queueMeshRenderItems(mesh *Mesh, passType internal.MeshRenderPassType) { - if !mesh.active { - return - } - definition := mesh.definition - passes := definition.passesByType[passType] - for _, pass := range passes { - r.renderItems = append(r.renderItems, renderItem{ - Layer: pass.Layer, - MaterialKey: pass.Key, - ArmatureKey: mesh.armature.key(), - - Pipeline: pass.Pipeline, - TextureSet: pass.TextureSet, - UniformSet: pass.UniformSet, - ModelData: mesh.matrixData, - ArmatureData: mesh.armature.uniformData(), - - IndexByteOffset: pass.IndexByteOffset, - IndexCount: pass.IndexCount, - }) - } -} - -func (r *sceneRenderer) queueStaticMeshRenderItems(ctx renderCtx, mesh *StaticMesh, passType internal.MeshRenderPassType) { - if !mesh.active { - return - } - distance := dprec.Vec3Diff(mesh.position, ctx.cameraPosition).Length() - if distance < mesh.minDistance || mesh.maxDistance < distance { - return - } - - // TODO: Extract common stuff between mesh and static mesh into a type - // that is passed ot this function instead so that it can be reused. - definition := mesh.definition - passes := definition.passesByType[passType] - for _, pass := range passes { - r.renderItems = append(r.renderItems, renderItem{ - Layer: pass.Layer, - MaterialKey: pass.Key, - ArmatureKey: math.MaxUint32, - - Pipeline: pass.Pipeline, - TextureSet: pass.TextureSet, - UniformSet: pass.UniformSet, - ModelData: mesh.matrixData, - ArmatureData: nil, - - IndexByteOffset: pass.IndexByteOffset, - IndexCount: pass.IndexCount, - }) - } -} - -func (r *sceneRenderer) renderMeshRenderItems(ctx renderMeshContext, items []renderItem) { - const maxBatchSize = modelUniformBufferItemCount - var ( - lastMaterialKey = uint32(math.MaxUint32) - lastArmatureKey = uint32(math.MaxUint32) - - batchStart = 0 - batchEnd = 0 - ) - - slices.SortFunc(items, compareMeshRenderItems) - - itemCount := len(items) - for i, item := range items { - materialKey := item.MaterialKey - armatureKey := item.ArmatureKey - - isSame := (materialKey == lastMaterialKey) && (armatureKey == lastArmatureKey) - if !isSame { - if batchStart < batchEnd { - r.renderMeshRenderItemBatch(ctx, items[batchStart:batchEnd]) - } - batchStart = batchEnd - } - batchEnd++ - - batchSize := batchEnd - batchStart - if (batchSize >= maxBatchSize) || (i == itemCount-1) { - r.renderMeshRenderItemBatch(ctx, items[batchStart:batchEnd]) - batchStart = batchEnd - } - - lastMaterialKey = materialKey - lastArmatureKey = armatureKey - } -} - -func (r *sceneRenderer) renderMeshRenderItemBatch(ctx renderMeshContext, items []renderItem) { - template := items[0] - - commandBuffer := r.stageData.CommandBuffer() - commandBuffer.BindPipeline(template.Pipeline) - - // Camera data is shared between all items. - cameraPlacement := ctx.CameraPlacement - commandBuffer.UniformBufferUnit( - internal.UniformBufferBindingCamera, - cameraPlacement.Buffer, - cameraPlacement.Offset, - cameraPlacement.Size, - ) - - // Material data is shared between all items. - uniformBuffer := r.stageData.UniformBuffer() - if !template.UniformSet.IsEmpty() { - materialPlacement := ubo.WriteUniform(uniformBuffer, internal.MaterialUniform{ - Data: template.UniformSet.Data(), - }) - commandBuffer.UniformBufferUnit( - internal.UniformBufferBindingMaterial, - materialPlacement.Buffer, - materialPlacement.Offset, - materialPlacement.Size, - ) - } - - for i := range template.TextureSet.TextureCount() { - if texture := template.TextureSet.TextureAt(i); texture != nil { - commandBuffer.TextureUnit(uint(i), texture) - } - if sampler := template.TextureSet.SamplerAt(i); sampler != nil { - commandBuffer.SamplerUnit(uint(i), sampler) - } - } - - // Model data needs to be combined. - for i, item := range items { - start := i * modelUniformBufferItemSize - end := start + modelUniformBufferItemSize - copy(r.modelUniformBufferData[start:end], item.ModelData) - } - modelPlacement := ubo.WriteUniform(uniformBuffer, internal.ModelUniform{ - ModelMatrices: r.modelUniformBufferData, - }) - commandBuffer.UniformBufferUnit( - internal.UniformBufferBindingModel, - modelPlacement.Buffer, - modelPlacement.Offset, - modelPlacement.Size, - ) - - // Armature data is shared between all items. - if template.ArmatureData != nil { - armaturePlacement := ubo.WriteUniform(uniformBuffer, internal.ArmatureUniform{ - BoneMatrices: template.ArmatureData, - }) - commandBuffer.UniformBufferUnit( - internal.UniformBufferBindingArmature, - armaturePlacement.Buffer, - armaturePlacement.Offset, - armaturePlacement.Size, - ) - } - - commandBuffer.DrawIndexed(template.IndexByteOffset, template.IndexCount, uint32(len(items))) -} - -func (r *sceneRenderer) renderAmbientLight(light *AmbientLight) { - quadShape := r.stageData.QuadShape() - commandBuffer := r.stageData.CommandBuffer() - // TODO: Ambient light intensity based on distance and inner and outer radius - commandBuffer.BindPipeline(r.ambientLightPipeline) - commandBuffer.TextureUnit(internal.TextureBindingLightingFramebufferColor0, r.geometryAlbedoTexture) - commandBuffer.SamplerUnit(internal.TextureBindingLightingFramebufferColor0, r.nearestSampler) - commandBuffer.TextureUnit(internal.TextureBindingLightingFramebufferColor1, r.geometryNormalTexture) - commandBuffer.SamplerUnit(internal.TextureBindingLightingFramebufferColor1, r.nearestSampler) - commandBuffer.TextureUnit(internal.TextureBindingLightingFramebufferDepth, r.geometryDepthTexture) - commandBuffer.SamplerUnit(internal.TextureBindingLightingFramebufferDepth, r.nearestSampler) - commandBuffer.TextureUnit(internal.TextureBindingLightingReflectionTexture, light.reflectionTexture) - commandBuffer.SamplerUnit(internal.TextureBindingLightingReflectionTexture, r.linearSampler) - commandBuffer.TextureUnit(internal.TextureBindingLightingRefractionTexture, light.refractionTexture) - commandBuffer.SamplerUnit(internal.TextureBindingLightingRefractionTexture, r.linearSampler) - commandBuffer.UniformBufferUnit( - internal.UniformBufferBindingCamera, - r.cameraPlacement.Buffer, - r.cameraPlacement.Offset, - r.cameraPlacement.Size, - ) - commandBuffer.DrawIndexed(0, quadShape.IndexCount(), 1) -} - -func (r *sceneRenderer) renderPointLight(light *PointLight) { - sphereShape := r.stageData.SphereShape() - projectionMatrix := sprec.IdentityMat4() - lightMatrix := light.gfxMatrix() - viewMatrix := sprec.InverseMat4(lightMatrix) - - uniformBuffer := r.stageData.UniformBuffer() - lightPlacement := ubo.WriteUniform(uniformBuffer, internal.LightUniform{ - ProjectionMatrix: projectionMatrix, - ViewMatrix: viewMatrix, - LightMatrix: lightMatrix, - }) - - lightPropertiesPlacement := ubo.WriteUniform(uniformBuffer, internal.LightPropertiesUniform{ - Color: dtos.Vec3(light.emitColor), - Intensity: 1.0, - Range: float32(light.emitRange), - }) - - commandBuffer := r.stageData.CommandBuffer() - commandBuffer.BindPipeline(r.pointLightPipeline) - commandBuffer.TextureUnit(internal.TextureBindingLightingFramebufferColor0, r.geometryAlbedoTexture) - commandBuffer.SamplerUnit(internal.TextureBindingLightingFramebufferColor0, r.nearestSampler) - commandBuffer.TextureUnit(internal.TextureBindingLightingFramebufferColor1, r.geometryNormalTexture) - commandBuffer.SamplerUnit(internal.TextureBindingLightingFramebufferColor1, r.nearestSampler) - commandBuffer.TextureUnit(internal.TextureBindingLightingFramebufferDepth, r.geometryDepthTexture) - commandBuffer.SamplerUnit(internal.TextureBindingLightingFramebufferDepth, r.nearestSampler) - commandBuffer.UniformBufferUnit( - internal.UniformBufferBindingCamera, - r.cameraPlacement.Buffer, - r.cameraPlacement.Offset, - r.cameraPlacement.Size, - ) - commandBuffer.UniformBufferUnit( - internal.UniformBufferBindingLight, - lightPlacement.Buffer, - lightPlacement.Offset, - lightPlacement.Size, - ) - commandBuffer.UniformBufferUnit( - internal.UniformBufferBindingLightProperties, - lightPropertiesPlacement.Buffer, - lightPropertiesPlacement.Offset, - lightPropertiesPlacement.Size, - ) - commandBuffer.DrawIndexed(0, sphereShape.IndexCount(), 1) -} - -func (r *sceneRenderer) renderSpotLight(light *SpotLight) { - coneShape := r.stageData.ConeShape() - projectionMatrix := sprec.IdentityMat4() - lightMatrix := light.gfxMatrix() - viewMatrix := sprec.InverseMat4(lightMatrix) - - uniformBuffer := r.stageData.UniformBuffer() - lightPlacement := ubo.WriteUniform(uniformBuffer, internal.LightUniform{ - ProjectionMatrix: projectionMatrix, - ViewMatrix: viewMatrix, - LightMatrix: lightMatrix, - }) - - lightPropertiesPlacement := ubo.WriteUniform(uniformBuffer, internal.LightPropertiesUniform{ - Color: dtos.Vec3(light.emitColor), - Intensity: 1.0, - Range: float32(light.emitRange), - OuterAngle: float32(light.emitOuterConeAngle.Radians()), - InnerAngle: float32(light.emitInnerConeAngle.Radians()), - }) - - commandBuffer := r.stageData.CommandBuffer() - commandBuffer.BindPipeline(r.spotLightPipeline) - commandBuffer.TextureUnit(internal.TextureBindingLightingFramebufferColor0, r.geometryAlbedoTexture) - commandBuffer.SamplerUnit(internal.TextureBindingLightingFramebufferColor0, r.nearestSampler) - commandBuffer.TextureUnit(internal.TextureBindingLightingFramebufferColor1, r.geometryNormalTexture) - commandBuffer.SamplerUnit(internal.TextureBindingLightingFramebufferColor1, r.nearestSampler) - commandBuffer.TextureUnit(internal.TextureBindingLightingFramebufferDepth, r.geometryDepthTexture) - commandBuffer.SamplerUnit(internal.TextureBindingLightingFramebufferDepth, r.nearestSampler) - commandBuffer.UniformBufferUnit( - internal.UniformBufferBindingCamera, - r.cameraPlacement.Buffer, - r.cameraPlacement.Offset, - r.cameraPlacement.Size, - ) - commandBuffer.UniformBufferUnit( - internal.UniformBufferBindingLight, - lightPlacement.Buffer, - lightPlacement.Offset, - lightPlacement.Size, - ) - commandBuffer.UniformBufferUnit( - internal.UniformBufferBindingLightProperties, - lightPropertiesPlacement.Buffer, - lightPropertiesPlacement.Offset, - lightPropertiesPlacement.Size, - ) - commandBuffer.DrawIndexed(0, coneShape.IndexCount(), 1) -} - -func (r *sceneRenderer) renderDirectionalLight(light *DirectionalLight) { - quadShape := r.stageData.QuadShape() - projectionMatrix := lightOrtho() - lightMatrix := light.gfxMatrix() - lightMatrix.M14 = sprec.Floor(lightMatrix.M14*shadowMapWidth) / float32(shadowMapWidth) - lightMatrix.M24 = sprec.Floor(lightMatrix.M24*shadowMapWidth) / float32(shadowMapWidth) - lightMatrix.M34 = sprec.Floor(lightMatrix.M34*shadowMapWidth) / float32(shadowMapWidth) - viewMatrix := sprec.InverseMat4(lightMatrix) - - uniformBuffer := r.stageData.UniformBuffer() - lightPlacement := ubo.WriteUniform(uniformBuffer, internal.LightUniform{ - ProjectionMatrix: projectionMatrix, - ViewMatrix: viewMatrix, - LightMatrix: lightMatrix, - }) - - lightPropertiesPlacement := ubo.WriteUniform(uniformBuffer, internal.LightPropertiesUniform{ - Color: dtos.Vec3(light.emitColor), - Intensity: 1.0, - }) - - commandBuffer := r.stageData.CommandBuffer() - commandBuffer.BindPipeline(r.directionalLightPipeline) - commandBuffer.TextureUnit(internal.TextureBindingLightingFramebufferColor0, r.geometryAlbedoTexture) - commandBuffer.SamplerUnit(internal.TextureBindingLightingFramebufferColor0, r.nearestSampler) - commandBuffer.TextureUnit(internal.TextureBindingLightingFramebufferColor1, r.geometryNormalTexture) - commandBuffer.SamplerUnit(internal.TextureBindingLightingFramebufferColor1, r.nearestSampler) - commandBuffer.TextureUnit(internal.TextureBindingLightingFramebufferDepth, r.geometryDepthTexture) - commandBuffer.SamplerUnit(internal.TextureBindingLightingFramebufferDepth, r.nearestSampler) - commandBuffer.TextureUnit(internal.TextureBindingShadowFramebufferDepth, r.shadowDepthTexture) - commandBuffer.SamplerUnit(internal.TextureBindingShadowFramebufferDepth, r.depthSampler) - commandBuffer.UniformBufferUnit( - internal.UniformBufferBindingCamera, - r.cameraPlacement.Buffer, - r.cameraPlacement.Offset, - r.cameraPlacement.Size, - ) - commandBuffer.UniformBufferUnit( - internal.UniformBufferBindingLight, - lightPlacement.Buffer, - lightPlacement.Offset, - lightPlacement.Size, - ) - commandBuffer.UniformBufferUnit( - internal.UniformBufferBindingLightProperties, - lightPropertiesPlacement.Buffer, - lightPropertiesPlacement.Offset, - lightPropertiesPlacement.Size, - ) - commandBuffer.DrawIndexed(0, quadShape.IndexCount(), 1) -} - -type renderCtx struct { - framebuffer render.Framebuffer - scene *Scene - x uint32 - y uint32 - width uint32 - height uint32 - camera *Camera - cameraPosition dprec.Vec3 - frustum spatial.HexahedronRegion - time float32 -} - -type renderMeshContext struct { - CameraPlacement ubo.UniformPlacement -} - // TODO: Rename to meshRenderItem type renderItem struct { Layer int32 @@ -1518,11 +248,3 @@ type renderItem struct { IndexByteOffset uint32 IndexCount uint32 } - -func compareMeshRenderItems(a, b renderItem) int { - return cmp.Or( - cmp.Compare(a.Layer, b.Layer), - cmp.Compare(a.MaterialKey, b.MaterialKey), - cmp.Compare(a.ArmatureKey, b.ArmatureKey), - ) -} diff --git a/game/graphics/scene.go b/game/graphics/scene.go index 535ed04f..3792950c 100644 --- a/game/graphics/scene.go +++ b/game/graphics/scene.go @@ -110,7 +110,7 @@ func (s *Scene) SetActiveCamera(camera *Camera) { // CreateCamera creates a new camera object to be // used with this scene. func (s *Scene) CreateCamera() *Camera { - result := newCamera() + result := newCamera(s) if s.activeCamera == nil { s.activeCamera = result } diff --git a/game/graphics/stage.go b/game/graphics/stage.go new file mode 100644 index 00000000..5c167f2d --- /dev/null +++ b/game/graphics/stage.go @@ -0,0 +1,97 @@ +package graphics + +import ( + "github.com/mokiat/gomath/dprec" + "github.com/mokiat/lacking/render" + "github.com/mokiat/lacking/render/ubo" + "github.com/mokiat/lacking/util/spatial" +) + +// Stage represents a render stage (e.g. geometry, lighting, post-processing). +type Stage interface { + + // Allocate is called once in the beginning to initialize any graphics + // resources. + Allocate() + + // Release is called once in the end to release any graphics resources. + Release() + + // PreRender is called before the stage renders its content. The width and + // height are in pixels and will never be zero or less. + PreRender(width, height uint32) + + // Render is called whenever the stage should render its content. + Render(ctx StageContext) + + // PostRender is called after all commands have been queued to the render API. + PostRender() +} + +// StageContext represents the context that is passed to a render stage. +type StageContext struct { + + // Scene is the scene that should be rendered by the stage. + Scene *Scene + + // Camera is the camera that should be used to render the stage. + Camera *Camera + + // CameraPosition is the position of the camera in world space. + CameraPosition dprec.Vec3 + + // CameraPlacement is the uniform buffer segment that contains the camera + // data. + CameraPlacement ubo.UniformPlacement + + // CameraFrustum is the frustum of the camera in world space. + CameraFrustum spatial.HexahedronRegion + + // VisibleAmbientLights is a list of ambient lights that are visible in the + // scene. + VisibleAmbientLights []*AmbientLight + + // VisiblePointLights is a list of point lights that are visible in the scene. + VisiblePointLights []*PointLight + + // VisibleSpotLights is a list of spot lights that are visible in the scene. + VisibleSpotLights []*SpotLight + + // VisibleDirectionalLights is a list of directional lights that are visible + // in the scene. + VisibleDirectionalLights []*DirectionalLight + + // VisibleMeshes is a list of meshes that are visible in the scene. + VisibleMeshes []*Mesh + + // VisibleStaticMeshIndices is a list of indices of static meshes that are + // visible in the scene. + VisibleStaticMeshIndices []uint32 + + // DebugLines is a list of debug lines that should be rendered by the stage. + // + // FIXME: Figure out a different way to do this. Maybe make it easy for + // users to emulate debug lines through forward pass and mutatable meshes? + DebugLines []DebugLine + + // Viewport is the area of the screen that the stage should render to. + // The width and height of the viewport will match the width and height + // that were passed to the PreRender method call. + Viewport render.Area + + // Framebuffer is the screen framebuffer. A stage would not normally use + // this unless it is the last stage in the rendering pipeline. + Framebuffer render.Framebuffer + + // CommandBuffer is the command buffer that the stage should use to queue + // rendering commands. + CommandBuffer render.CommandBuffer + + // UniformBuffer is the uniform buffer that the stage should use to set + // uniform data. + UniformBuffer *ubo.UniformBlockBuffer +} + +// StageTextureParameter is a function that returns a texture that is used as +// a parameter to a render stage. +type StageTextureParameter func() render.Texture diff --git a/game/graphics/stage_bloom.go b/game/graphics/stage_bloom.go index a1b3e8b4..c250eb9a 100644 --- a/game/graphics/stage_bloom.go +++ b/game/graphics/stage_bloom.go @@ -1,6 +1,8 @@ package graphics import ( + "github.com/mokiat/gog/opt" + "github.com/mokiat/lacking/debug/metric" "github.com/mokiat/lacking/game/graphics/internal" "github.com/mokiat/lacking/render" "github.com/mokiat/lacking/render/ubo" @@ -8,27 +10,38 @@ import ( const ( bloomDownsampleHDRImageSlot = 0 + bloomBlurSourceImageSlot = 0 - bloomBlurSourceImageSlot = 0 + bloomDefaultBlurIterations = 2 ) -const ( - blurIterations = 2 -) +// BloomStageInput is used to configure a BloomStage. +type BloomStageInput struct { + HDRTexture StageTextureParameter +} -func newBloomRenderStage(api render.API, shaders ShaderCollection, data *commonStageData) *bloomRenderStage { - return &bloomRenderStage{ +func newBloomStage(api render.API, shaders ShaderCollection, data *commonStageData, input BloomStageInput) *BloomStage { + return &BloomStage{ api: api, shaders: shaders, data: data, + + inHDRTexture: input.HDRTexture, + iterations: bloomDefaultBlurIterations, } } -type bloomRenderStage struct { +var _ Stage = (*BloomStage)(nil) + +// BloomStage is a stage that produces a bloom overlay texture. +type BloomStage struct { api render.API shaders ShaderCollection data *commonStageData + inHDRTexture StageTextureParameter + iterations int + framebufferWidth uint32 framebufferHeight uint32 @@ -38,9 +51,6 @@ type bloomRenderStage struct { pongFramebuffer render.Framebuffer pongTexture render.Texture - outputTexture render.Texture - outputSampler render.Sampler - downsampleProgram render.Program downsamplePipeline render.Pipeline downsampleSampler render.Sampler @@ -50,7 +60,24 @@ type bloomRenderStage struct { blurSampler render.Sampler } -func (s *bloomRenderStage) Allocate() { +// Iterations returns the number of blur iterations that are +// performed on the bloom overlay texture. +func (s *BloomStage) Iterations() int { + return s.iterations +} + +// SetIterations sets the number of blur iterations that should be +// performed on the bloom overlay texture. +func (s *BloomStage) SetIterations(iterations int) { + s.iterations = iterations +} + +// BloomTexture returns the texture that contains the bloom overlay. +func (s *BloomStage) BloomTexture() render.Texture { + return s.pingTexture +} + +func (s *BloomStage) Allocate() { quadShape := s.data.QuadShape() s.allocateTextures(1, 1) @@ -107,16 +134,9 @@ func (s *bloomRenderStage) Allocate() { Filtering: render.FilterModeNearest, Mipmapping: false, }) - - s.outputTexture = s.pingTexture - s.outputSampler = s.api.CreateSampler(render.SamplerInfo{ - Wrapping: render.WrapModeClamp, - Filtering: render.FilterModeLinear, - Mipmapping: false, - }) } -func (s *bloomRenderStage) Release() { +func (s *BloomStage) Release() { defer s.releaseTextures() defer s.downsampleProgram.Release() @@ -126,22 +146,23 @@ func (s *bloomRenderStage) Release() { defer s.blurProgram.Release() defer s.blurPipeline.Release() defer s.blurSampler.Release() - - defer s.outputSampler.Release() } -func (s *bloomRenderStage) Resize(width, height uint32) { - width = max(1, width/2) - height = max(1, height/2) - - s.releaseTextures() - s.allocateTextures(width, height) +func (s *BloomStage) PreRender(width, height uint32) { + targetWidth := max(1, width/2) + targetHeight := max(1, height/2) + if s.framebufferWidth != targetWidth || s.framebufferHeight != targetHeight { + s.releaseTextures() + s.allocateTextures(targetWidth, targetHeight) + } } -func (s *bloomRenderStage) Run(hdrImage render.Texture) { - commandBuffer := s.data.CommandBuffer() - uniformBuffer := s.data.UniformBuffer() +func (s *BloomStage) Render(ctx StageContext) { + defer metric.BeginRegion("bloom").End() + quadShape := s.data.QuadShape() + commandBuffer := ctx.CommandBuffer + uniformBuffer := ctx.UniformBuffer // Perform a downsample into the Ping texture commandBuffer.BeginRenderPass(render.RenderPassInfo{ @@ -163,14 +184,14 @@ func (s *bloomRenderStage) Run(hdrImage render.Texture) { }, }) commandBuffer.BindPipeline(s.downsamplePipeline) - commandBuffer.TextureUnit(bloomDownsampleHDRImageSlot, hdrImage) + commandBuffer.TextureUnit(bloomDownsampleHDRImageSlot, s.inHDRTexture()) commandBuffer.SamplerUnit(bloomDownsampleHDRImageSlot, s.downsampleSampler) commandBuffer.DrawIndexed(0, quadShape.IndexCount(), 1) commandBuffer.EndRenderPass() // Perform blur passes horizontal := float32(1.0) - for range blurIterations * 2 { + for range s.iterations * 2 { // times 2 because we do horizontal and vertical passes commandBuffer.BeginRenderPass(render.RenderPassInfo{ Framebuffer: s.pongFramebuffer, Viewport: render.Area{ @@ -203,19 +224,13 @@ func (s *bloomRenderStage) Run(hdrImage render.Texture) { s.pingTexture, s.pongTexture = s.pongTexture, s.pingTexture horizontal = 1.0 - horizontal } - - s.outputTexture = s.pingTexture -} - -func (s *bloomRenderStage) OutputTexture() render.Texture { - return s.outputTexture } -func (s *bloomRenderStage) OutputSampler() render.Sampler { - return s.outputSampler +func (s *BloomStage) PostRender() { + // Nothing to do here. } -func (s *bloomRenderStage) allocateTextures(width, height uint32) { +func (s *BloomStage) allocateTextures(width, height uint32) { s.framebufferWidth = width s.framebufferHeight = height @@ -227,8 +242,8 @@ func (s *bloomRenderStage) allocateTextures(width, height uint32) { Format: render.DataFormatRGBA16F, }) s.pingFramebuffer = s.api.CreateFramebuffer(render.FramebufferInfo{ - ColorAttachments: [4]render.Texture{ - s.pingTexture, + ColorAttachments: [4]opt.T[render.TextureAttachment]{ + opt.V(render.PlainTextureAttachment(s.pingTexture)), }, }) @@ -240,13 +255,13 @@ func (s *bloomRenderStage) allocateTextures(width, height uint32) { Format: render.DataFormatRGBA16F, }) s.pongFramebuffer = s.api.CreateFramebuffer(render.FramebufferInfo{ - ColorAttachments: [4]render.Texture{ - s.pongTexture, + ColorAttachments: [4]opt.T[render.TextureAttachment]{ + opt.V(render.PlainTextureAttachment(s.pongTexture)), }, }) } -func (s *bloomRenderStage) releaseTextures() { +func (s *BloomStage) releaseTextures() { defer s.pingFramebuffer.Release() defer s.pingTexture.Release() diff --git a/game/graphics/stage_builder.go b/game/graphics/stage_builder.go new file mode 100644 index 00000000..71cf301d --- /dev/null +++ b/game/graphics/stage_builder.go @@ -0,0 +1,61 @@ +package graphics + +import "github.com/mokiat/gog/opt" + +// StageBuilderFunc is a function that creates a list of stages. +type StageBuilderFunc func(provider *StageProvider) []Stage + +// DefaultStageBuilder is a default implementation of a stage builder. +func DefaultStageBuilder(provider *StageProvider) []Stage { + depthSourceStage := provider.CreateDepthSourceStage() + + geometrySourceStage := provider.CreateGeometrySourceStage() + + forwardSourceStage := provider.CreateForwardSourceStage() + + shadowStage := provider.CreateShadowStage() + + geometryStage := provider.CreateGeometryStage(GeometryStageInput{ + AlbedoMetallicTexture: geometrySourceStage.AlbedoMetallicTexture, + NormalRoughnessTexture: geometrySourceStage.NormalRoughnessTexture, + DepthTexture: depthSourceStage.DepthTexture, + }) + + lightingStage := provider.CreateLightingStage(LightingStageInput{ + AlbedoMetallicTexture: geometrySourceStage.AlbedoMetallicTexture, + NormalRoughnessTexture: geometrySourceStage.NormalRoughnessTexture, + DepthTexture: depthSourceStage.DepthTexture, + HDRTexture: forwardSourceStage.HDRTexture, + }) + + forwardStage := provider.CreateForwardStage(ForwardStageInput{ + HDRTexture: forwardSourceStage.HDRTexture, + DepthTexture: depthSourceStage.DepthTexture, + }) + + exposureProbeStage := provider.CreateExposureProbeStage(ExposureProbeStageInput{ + HDRTexture: forwardSourceStage.HDRTexture, + }) + + bloomStage := provider.CreateBloomStage(BloomStageInput{ + HDRTexture: forwardSourceStage.HDRTexture, + }) + + toneMappingStage := provider.CreateToneMappingStage(ToneMappingStageInput{ + HDRTexture: forwardSourceStage.HDRTexture, + BloomTexture: opt.V[StageTextureParameter](bloomStage.BloomTexture), + }) + + return []Stage{ + depthSourceStage, + geometrySourceStage, + forwardSourceStage, + shadowStage, + geometryStage, + lightingStage, + forwardStage, + exposureProbeStage, + bloomStage, + toneMappingStage, + } +} diff --git a/game/graphics/stage_common.go b/game/graphics/stage_common.go index 1885e61f..9cbd6836 100644 --- a/game/graphics/stage_common.go +++ b/game/graphics/stage_common.go @@ -1,14 +1,32 @@ package graphics import ( + "github.com/mokiat/gog" + "github.com/mokiat/gog/opt" "github.com/mokiat/lacking/game/graphics/internal" "github.com/mokiat/lacking/render" "github.com/mokiat/lacking/render/ubo" ) -func newCommonStageData(api render.API) *commonStageData { +func newCommonStageData(api render.API, cfg *config) *commonStageData { return &commonStageData{ api: api, + + directionalShadowMapCount: cfg.DirectionalShadowMapCount, + directionalShadowMapSize: cfg.DirectionalShadowMapSize, + directionalShadowMapCascadeCount: cfg.DirectionalShadowMapCascadeCount, + directionalShadowMaps: make([]internal.DirectionalShadowMap, cfg.DirectionalShadowMapCount), + directionalShadowMapAssignments: make(map[*DirectionalLight]*internal.DirectionalShadowMap, cfg.DirectionalShadowMapCount), + + spotShadowMapCount: cfg.SpotShadowMapCount, + spotShadowMapSize: cfg.SpotShadowMapSize, + spotShadowMaps: make([]internal.SpotShadowMap, cfg.SpotShadowMapCount), + spotShadowMapAssignments: make(map[*SpotLight]*internal.SpotShadowMap, cfg.SpotShadowMapCount), + + pointShadowMapCount: cfg.PointShadowMapCount, + pointShadowMapSize: cfg.PointShadowMapSize, + pointShadowMaps: make([]internal.PointShadowMap, cfg.PointShadowMapCount), + pointShadowMapAssignments: make(map[*PointLight]*internal.PointShadowMap, cfg.PointShadowMapCount), } } @@ -20,8 +38,28 @@ type commonStageData struct { sphereShape *internal.Shape coneShape *internal.Shape + nearestSampler render.Sampler + linearSampler render.Sampler + depthSampler render.Sampler + commandBuffer render.CommandBuffer uniformBuffer *ubo.UniformBlockBuffer + + directionalShadowMapCount int + directionalShadowMapSize int + directionalShadowMapCascadeCount int + directionalShadowMaps []internal.DirectionalShadowMap + directionalShadowMapAssignments map[*DirectionalLight]*internal.DirectionalShadowMap + + spotShadowMapCount int + spotShadowMapSize int + spotShadowMaps []internal.SpotShadowMap + spotShadowMapAssignments map[*SpotLight]*internal.SpotShadowMap + + pointShadowMapCount int + pointShadowMapSize int + pointShadowMaps []internal.PointShadowMap + pointShadowMapAssignments map[*PointLight]*internal.PointShadowMap } func (d *commonStageData) Allocate() { @@ -30,8 +68,71 @@ func (d *commonStageData) Allocate() { d.sphereShape = internal.CreateSphereShape(d.api) d.coneShape = internal.CreateConeShape(d.api) + d.nearestSampler = d.api.CreateSampler(render.SamplerInfo{ + Wrapping: render.WrapModeClamp, + Filtering: render.FilterModeNearest, + Mipmapping: false, + }) + d.linearSampler = d.api.CreateSampler(render.SamplerInfo{ + Wrapping: render.WrapModeClamp, + Filtering: render.FilterModeLinear, + Mipmapping: false, + }) + d.depthSampler = d.api.CreateSampler(render.SamplerInfo{ + Wrapping: render.WrapModeClamp, + Filtering: render.FilterModeLinear, + Comparison: opt.V(render.ComparisonLess), + Mipmapping: false, + }) + d.commandBuffer = d.api.CreateCommandBuffer(commandBufferSize) d.uniformBuffer = ubo.NewUniformBlockBuffer(d.api, uniformBufferSize) + + gog.Mutate(d.directionalShadowMaps, func(shadowMap *internal.DirectionalShadowMap) { + shadowMap.ArrayTexture = d.api.CreateDepthTexture2DArray(render.DepthTexture2DArrayInfo{ + Width: uint32(d.directionalShadowMapSize), + Height: uint32(d.directionalShadowMapSize), + Layers: uint32(d.directionalShadowMapCascadeCount), + Comparable: true, + }) + shadowMap.Cascades = make([]internal.DirectionalShadowMapCascade, d.directionalShadowMapCascadeCount) + gog.MutateIndex(shadowMap.Cascades, func(j int, cascade *internal.DirectionalShadowMapCascade) { + cascade.Framebuffer = d.api.CreateFramebuffer(render.FramebufferInfo{ + DepthAttachment: opt.V(render.TextureAttachment{ + Texture: shadowMap.ArrayTexture, + Depth: uint32(j), + }), + }) + }) + }) + + gog.Mutate(d.spotShadowMaps, func(shadowMap *internal.SpotShadowMap) { + shadowMap.Texture = d.api.CreateDepthTexture2D(render.DepthTexture2DInfo{ + Width: uint32(d.spotShadowMapSize), + Height: uint32(d.spotShadowMapSize), + Comparable: true, + }) + shadowMap.Framebuffer = d.api.CreateFramebuffer(render.FramebufferInfo{ + DepthAttachment: opt.V(render.PlainTextureAttachment(shadowMap.Texture)), + }) + }) + + gog.Mutate(d.pointShadowMaps, func(shadowMap *internal.PointShadowMap) { + shadowMap.ArrayTexture = d.api.CreateDepthTexture2DArray(render.DepthTexture2DArrayInfo{ + Width: uint32(d.pointShadowMapSize), + Height: uint32(d.pointShadowMapSize), + Layers: 6, + Comparable: true, + }) + for i := range 6 { + shadowMap.Framebuffers[i] = d.api.CreateFramebuffer(render.FramebufferInfo{ + DepthAttachment: opt.V(render.TextureAttachment{ + Texture: shadowMap.ArrayTexture, + Depth: uint32(i), + }), + }) + } + }) } func (d *commonStageData) Release() { @@ -40,7 +141,30 @@ func (d *commonStageData) Release() { defer d.sphereShape.Release() defer d.coneShape.Release() + defer d.nearestSampler.Release() + defer d.linearSampler.Release() + defer d.depthSampler.Release() + defer d.uniformBuffer.Release() + + gog.Mutate(d.directionalShadowMaps, func(shadowMap *internal.DirectionalShadowMap) { + defer shadowMap.ArrayTexture.Release() + for _, cascade := range shadowMap.Cascades { + defer cascade.Framebuffer.Release() + } + }) + + gog.Mutate(d.spotShadowMaps, func(shadowMap *internal.SpotShadowMap) { + defer shadowMap.Texture.Release() + defer shadowMap.Framebuffer.Release() + }) + + gog.Mutate(d.pointShadowMaps, func(shadowMap *internal.PointShadowMap) { + defer shadowMap.ArrayTexture.Release() + for _, framebuffer := range shadowMap.Framebuffers { + defer framebuffer.Release() + } + }) } func (d *commonStageData) CommandBuffer() render.CommandBuffer { @@ -66,3 +190,69 @@ func (d *commonStageData) SphereShape() *internal.Shape { func (d *commonStageData) ConeShape() *internal.Shape { return d.coneShape } + +func (d *commonStageData) NearestSampler() render.Sampler { + return d.nearestSampler +} + +func (d *commonStageData) LinearSampler() render.Sampler { + return d.linearSampler +} + +func (d *commonStageData) DepthSampler() render.Sampler { + return d.depthSampler +} + +func (d *commonStageData) ResetDirectionalShadowMapAssignments() { + clear(d.directionalShadowMapAssignments) +} + +func (d *commonStageData) AssignDirectionalShadowMap(light *DirectionalLight) *internal.DirectionalShadowMap { + freeIndex := len(d.directionalShadowMapAssignments) + if freeIndex >= len(d.directionalShadowMaps) { + return nil + } + shadowMap := &d.directionalShadowMaps[freeIndex] + d.directionalShadowMapAssignments[light] = shadowMap + return shadowMap +} + +func (d *commonStageData) GetDirectionalShadowMap(light *DirectionalLight) *internal.DirectionalShadowMap { + return d.directionalShadowMapAssignments[light] +} + +func (d *commonStageData) ResetSpotShadowMapAssignments() { + clear(d.spotShadowMapAssignments) +} + +func (d *commonStageData) AssignSpotShadowMap(light *SpotLight) *internal.SpotShadowMap { + freeIndex := len(d.spotShadowMapAssignments) + if freeIndex >= len(d.spotShadowMaps) { + return nil + } + shadowMap := &d.spotShadowMaps[freeIndex] + d.spotShadowMapAssignments[light] = shadowMap + return shadowMap +} + +func (d *commonStageData) GetSpotShadowMap(light *SpotLight) *internal.SpotShadowMap { + return d.spotShadowMapAssignments[light] +} + +func (d *commonStageData) ResetPointShadowMapAssignments() { + clear(d.pointShadowMapAssignments) +} + +func (d *commonStageData) AssignPointShadowMap(light *PointLight) *internal.PointShadowMap { + freeIndex := len(d.pointShadowMapAssignments) + if freeIndex >= len(d.pointShadowMaps) { + return nil + } + shadowMap := &d.pointShadowMaps[freeIndex] + d.pointShadowMapAssignments[light] = shadowMap + return shadowMap +} + +func (d *commonStageData) GetPointShadowMap(light *PointLight) *internal.PointShadowMap { + return d.pointShadowMapAssignments[light] +} diff --git a/game/graphics/stage_depth_source.go b/game/graphics/stage_depth_source.go new file mode 100644 index 00000000..30c3faa6 --- /dev/null +++ b/game/graphics/stage_depth_source.go @@ -0,0 +1,64 @@ +package graphics + +import "github.com/mokiat/lacking/render" + +func newDepthSourceStage(api render.API) *DepthSourceStage { + return &DepthSourceStage{ + api: api, + } +} + +var _ Stage = (*DepthSourceStage)(nil) + +// DepthSourceStage is a stage that provides a depth source texture. +type DepthSourceStage struct { + api render.API + + framebufferWidth uint32 + framebufferHeight uint32 + + depthTexture render.Texture +} + +// DepthTexture returns the texture that contains the depth information. +func (s *DepthSourceStage) DepthTexture() render.Texture { + return s.depthTexture +} + +func (s *DepthSourceStage) Allocate() { + s.framebufferWidth = 32 + s.framebufferHeight = 32 + s.allocateTextures() +} + +func (s *DepthSourceStage) Release() { + defer s.releaseTextures() +} + +func (s *DepthSourceStage) PreRender(width, height uint32) { + if s.framebufferWidth != width || s.framebufferHeight != height { + s.framebufferWidth = width + s.framebufferHeight = height + s.releaseTextures() + s.allocateTextures() + } +} + +func (s *DepthSourceStage) Render(ctx StageContext) { + // Nothing to do here. +} + +func (s *DepthSourceStage) PostRender() { + // Nothing to do here. +} + +func (s *DepthSourceStage) allocateTextures() { + s.depthTexture = s.api.CreateDepthTexture2D(render.DepthTexture2DInfo{ + Width: s.framebufferWidth, + Height: s.framebufferHeight, + }) +} + +func (s *DepthSourceStage) releaseTextures() { + defer s.depthTexture.Release() +} diff --git a/game/graphics/stage_forward.go b/game/graphics/stage_forward.go new file mode 100644 index 00000000..058750e7 --- /dev/null +++ b/game/graphics/stage_forward.go @@ -0,0 +1,270 @@ +package graphics + +import ( + "github.com/mokiat/gog/ds" + "github.com/mokiat/gog/opt" + "github.com/mokiat/lacking/debug/metric" + "github.com/mokiat/lacking/game/graphics/internal" + "github.com/mokiat/lacking/render" + "github.com/mokiat/lacking/render/ubo" + "github.com/mokiat/lacking/util/blob" +) + +const ( + debugVertexSize = 3*render.SizeF32 + 4*render.SizeU8 + debugBufferSize = 1024 * 1024 + debugMaxLineCount = debugBufferSize / ((debugVertexSize * 2) * 2) // dobule buffer +) + +type ForwardStageInput struct { + HDRTexture StageTextureParameter + DepthTexture StageTextureParameter +} + +func newForwardStage(api render.API, shaders ShaderCollection, data *commonStageData, meshRenderer *meshRenderer, input ForwardStageInput) *ForwardStage { + return &ForwardStage{ + api: api, + shaders: shaders, + data: data, + input: input, + meshRenderer: meshRenderer, + } +} + +var _ Stage = (*ForwardStage)(nil) + +type ForwardStage struct { + api render.API + shaders ShaderCollection + data *commonStageData + meshRenderer *meshRenderer + input ForwardStageInput + + hdrTexture render.Texture + depthTexture render.Texture + framebuffer render.Framebuffer + + debugVertexData []byte + debugVertexBuffer render.Buffer + debugVertexBufferOffset uint32 + debugVertexArray render.VertexArray + debugProgram render.Program + debugPipeline render.Pipeline +} + +func (s *ForwardStage) Allocate() { + s.allocateFramebuffer() + s.allocateDebug() +} + +func (s *ForwardStage) Release() { + defer s.releaseFramebuffer() + defer s.releaseDebug() +} + +func (s *ForwardStage) PreRender(width, height uint32) { + hdrTexture := s.input.HDRTexture() + depthTexture := s.input.DepthTexture() + if hdrTexture != s.hdrTexture || depthTexture != s.depthTexture { + s.releaseFramebuffer() + s.allocateFramebuffer() + } +} + +func (s *ForwardStage) Render(ctx StageContext) { + defer metric.BeginRegion("forward").End() + + commandBuffer := ctx.CommandBuffer + commandBuffer.BeginRenderPass(render.RenderPassInfo{ + Framebuffer: s.framebuffer, + Viewport: render.Area{ + Width: s.hdrTexture.Width(), + Height: s.hdrTexture.Height(), + }, + DepthLoadOp: render.LoadOperationLoad, + DepthStoreOp: render.StoreOperationStore, + StencilLoadOp: render.LoadOperationLoad, + StencilStoreOp: render.StoreOperationDiscard, + Colors: [4]render.ColorAttachmentInfo{ + { + LoadOp: render.LoadOperationLoad, + StoreOp: render.StoreOperationStore, + }, + }, + }) + s.renderSky(ctx) + s.renderDebug(ctx) + s.renderMeshes(ctx) + commandBuffer.EndRenderPass() +} + +func (s *ForwardStage) PostRender() { + // Nothing to do here. +} + +func (s *ForwardStage) allocateFramebuffer() { + s.hdrTexture = s.input.HDRTexture() + s.depthTexture = s.input.DepthTexture() + + s.framebuffer = s.api.CreateFramebuffer(render.FramebufferInfo{ + ColorAttachments: [4]opt.T[render.TextureAttachment]{ + opt.V(render.PlainTextureAttachment(s.hdrTexture)), + }, + DepthAttachment: opt.V(render.PlainTextureAttachment(s.depthTexture)), + }) +} + +func (s *ForwardStage) releaseFramebuffer() { + defer s.framebuffer.Release() +} + +func (s *ForwardStage) allocateDebug() { + s.debugVertexData = make([]byte, debugBufferSize) + s.debugVertexBuffer = s.api.CreateVertexBuffer(render.BufferInfo{ + Dynamic: true, + Data: s.debugVertexData, + }) + + const coordOffset = 0 + const colorOffset = coordOffset + 3*render.SizeF32 + s.debugVertexArray = s.api.CreateVertexArray(render.VertexArrayInfo{ + Bindings: []render.VertexArrayBinding{ + render.NewVertexArrayBinding(s.debugVertexBuffer, debugVertexSize), + }, + Attributes: []render.VertexArrayAttribute{ + render.NewVertexArrayAttribute(0, internal.CoordAttributeIndex, coordOffset, render.VertexAttributeFormatRGB32F), + render.NewVertexArrayAttribute(0, internal.ColorAttributeIndex, colorOffset, render.VertexAttributeFormatRGBA8UN), + }, + }) + + s.debugProgram = s.api.CreateProgram(render.ProgramInfo{ + SourceCode: s.shaders.DebugSet(), + TextureBindings: []render.TextureBinding{}, + UniformBindings: []render.UniformBinding{ + render.NewUniformBinding("Camera", internal.UniformBufferBindingCamera), + }, + }) + s.debugPipeline = s.api.CreatePipeline(render.PipelineInfo{ + Program: s.debugProgram, + VertexArray: s.debugVertexArray, + Topology: render.TopologyLineList, + Culling: render.CullModeNone, + FrontFace: render.FaceOrientationCCW, + DepthTest: true, + DepthWrite: false, + DepthComparison: render.ComparisonLessOrEqual, + StencilTest: false, + ColorWrite: render.ColorMaskTrue, + BlendEnabled: false, + }) +} + +func (s *ForwardStage) releaseDebug() { + defer s.debugVertexBuffer.Release() + defer s.debugVertexArray.Release() + defer s.debugProgram.Release() + defer s.debugPipeline.Release() +} + +func (s *ForwardStage) renderSky(ctx StageContext) { + sky := s.findActiveSky(ctx.Scene.skies) + if sky == nil { + return + } + + commandBuffer := ctx.CommandBuffer + uniformBuffer := ctx.UniformBuffer + + for _, pass := range sky.definition.renderPasses { + commandBuffer.BindPipeline(pass.Pipeline) + commandBuffer.UniformBufferUnit( + internal.UniformBufferBindingCamera, + ctx.CameraPlacement.Buffer, + ctx.CameraPlacement.Offset, + ctx.CameraPlacement.Size, + ) + if !pass.UniformSet.IsEmpty() { + materialData := ubo.WriteUniform(uniformBuffer, internal.MaterialUniform{ + Data: pass.UniformSet.Data(), + }) + commandBuffer.UniformBufferUnit( + internal.UniformBufferBindingMaterial, + materialData.Buffer, + materialData.Offset, + materialData.Size, + ) + } + for i := range pass.TextureSet.TextureCount() { + if texture := pass.TextureSet.TextureAt(i); texture != nil { + commandBuffer.TextureUnit(uint(i), texture) + } + if sampler := pass.TextureSet.SamplerAt(i); sampler != nil { + commandBuffer.SamplerUnit(uint(i), sampler) + } + } + commandBuffer.DrawIndexed(pass.IndexByteOffset, pass.IndexCount, 1) + } +} + +func (s *ForwardStage) renderDebug(ctx StageContext) { + count := len(ctx.DebugLines) + if count == 0 { + return + } + + plotter := blob.NewPlotter(s.debugVertexData) + for _, line := range ctx.DebugLines { + plotter.PlotSPVec3(line.Start) + plotter.PlotUint8(uint8(line.Color.X * 255)) + plotter.PlotUint8(uint8(line.Color.Y * 255)) + plotter.PlotUint8(uint8(line.Color.Z * 255)) + plotter.PlotUint8(uint8(255)) + + plotter.PlotSPVec3(line.End) + plotter.PlotUint8(uint8(line.Color.X * 255)) + plotter.PlotUint8(uint8(line.Color.Y * 255)) + plotter.PlotUint8(uint8(line.Color.Z * 255)) + plotter.PlotUint8(uint8(255)) + } + vertexData := s.debugVertexData[:plotter.Offset()] + s.api.Queue().WriteBuffer(s.debugVertexBuffer, s.debugVertexBufferOffset, vertexData) + + commandBuffer := ctx.CommandBuffer + commandBuffer.BindPipeline(s.debugPipeline) + commandBuffer.UniformBufferUnit( + internal.UniformBufferBindingCamera, + ctx.CameraPlacement.Buffer, + ctx.CameraPlacement.Offset, + ctx.CameraPlacement.Size, + ) + commandBuffer.Draw(0, uint32(count)*2, 1) + + // Double buffer the vertex buffer through offset and hope the driver + // is smart enough to figure this out. + if s.debugVertexBufferOffset == 0 { + s.debugVertexBufferOffset = uint32(len(s.debugVertexData)) / 2 + } else { + s.debugVertexBufferOffset = 0 + } +} + +func (s *ForwardStage) renderMeshes(ctx StageContext) { + s.meshRenderer.DiscardRenderItems() + for _, mesh := range ctx.VisibleMeshes { + s.meshRenderer.QueueMeshRenderItems(ctx, mesh, internal.MeshRenderPassTypeForward) + } + for _, meshIndex := range ctx.VisibleStaticMeshIndices { + staticMesh := &ctx.Scene.staticMeshes[meshIndex] + s.meshRenderer.QueueStaticMeshRenderItems(ctx, staticMesh, internal.MeshRenderPassTypeForward) + } + s.meshRenderer.Render(ctx) +} + +func (s *ForwardStage) findActiveSky(skies *ds.List[*Sky]) *Sky { + for _, sky := range skies.Unbox() { + if sky.Active() { + return sky + } + } + return nil +} diff --git a/game/graphics/stage_forward_source.go b/game/graphics/stage_forward_source.go new file mode 100644 index 00000000..6c4d96b7 --- /dev/null +++ b/game/graphics/stage_forward_source.go @@ -0,0 +1,69 @@ +package graphics + +import "github.com/mokiat/lacking/render" + +func newForwardSourceStage(api render.API) *ForwardSourceStage { + return &ForwardSourceStage{ + api: api, + } +} + +var _ Stage = (*ForwardSourceStage)(nil) + +// ForwardSourceStage is a stage that provides source textures for +// a forward pass renderer. +type ForwardSourceStage struct { + api render.API + + framebufferWidth uint32 + framebufferHeight uint32 + + hdrTexture render.Texture +} + +// HDRTexture returns the texture that contains the high dynamic range +// color information. +func (s *ForwardSourceStage) HDRTexture() render.Texture { + return s.hdrTexture +} + +func (s *ForwardSourceStage) Allocate() { + s.framebufferWidth = 32 + s.framebufferHeight = 32 + s.allocateTextures() +} + +func (s *ForwardSourceStage) Release() { + defer s.releaseTextures() +} + +func (s *ForwardSourceStage) PreRender(width, height uint32) { + if s.framebufferWidth != width || s.framebufferHeight != height { + s.framebufferWidth = width + s.framebufferHeight = height + s.releaseTextures() + s.allocateTextures() + } +} + +func (s *ForwardSourceStage) Render(ctx StageContext) { + // Nothing to do here. +} + +func (s *ForwardSourceStage) PostRender() { + // Nothing to do here. +} + +func (s *ForwardSourceStage) allocateTextures() { + s.hdrTexture = s.api.CreateColorTexture2D(render.ColorTexture2DInfo{ + Width: s.framebufferWidth, + Height: s.framebufferHeight, + GenerateMipmaps: false, + GammaCorrection: false, + Format: render.DataFormatRGBA16F, + }) +} + +func (s *ForwardSourceStage) releaseTextures() { + defer s.hdrTexture.Release() +} diff --git a/game/graphics/stage_geometry.go b/game/graphics/stage_geometry.go new file mode 100644 index 00000000..e31f63e7 --- /dev/null +++ b/game/graphics/stage_geometry.go @@ -0,0 +1,122 @@ +package graphics + +import ( + "github.com/mokiat/gog/opt" + "github.com/mokiat/lacking/debug/metric" + "github.com/mokiat/lacking/game/graphics/internal" + "github.com/mokiat/lacking/render" +) + +// GeometryStageInput is used to configure a new GeometryStage. +type GeometryStageInput struct { + AlbedoMetallicTexture StageTextureParameter + NormalRoughnessTexture StageTextureParameter + DepthTexture StageTextureParameter +} + +func newGeometryStage(api render.API, meshRenderer *meshRenderer, input GeometryStageInput) *GeometryStage { + return &GeometryStage{ + api: api, + meshRenderer: meshRenderer, + input: input, + } +} + +var _ Stage = (*GeometryStage)(nil) + +// GeometryStage is a render stage that renders the geometry of the scene. +type GeometryStage struct { + api render.API + meshRenderer *meshRenderer + input GeometryStageInput + + albedoMetallicTexture render.Texture + normalRoughnessTexture render.Texture + depthTexture render.Texture + framebuffer render.Framebuffer +} + +func (s *GeometryStage) Allocate() { + s.allocateFramebuffer() +} + +func (s *GeometryStage) Release() { + defer s.releaseFramebuffer() +} + +func (s *GeometryStage) PreRender(width, height uint32) { + albedoMetallicTexture := s.input.AlbedoMetallicTexture() + normalRoughnessTexture := s.input.NormalRoughnessTexture() + depthTexture := s.input.DepthTexture() + if albedoMetallicTexture != s.albedoMetallicTexture || normalRoughnessTexture != s.normalRoughnessTexture || depthTexture != s.depthTexture { + s.releaseFramebuffer() + s.allocateFramebuffer() + } +} + +func (s *GeometryStage) Render(ctx StageContext) { + defer metric.BeginRegion("geometry").End() + + commandBuffer := ctx.CommandBuffer + commandBuffer.BeginRenderPass(render.RenderPassInfo{ + Framebuffer: s.framebuffer, + Viewport: render.Area{ + Width: s.albedoMetallicTexture.Width(), + Height: s.albedoMetallicTexture.Height(), + }, + DepthLoadOp: render.LoadOperationClear, + DepthStoreOp: render.StoreOperationStore, + DepthClearValue: 1.0, + StencilLoadOp: render.LoadOperationLoad, + StencilStoreOp: render.StoreOperationDiscard, + Colors: [4]render.ColorAttachmentInfo{ + { + LoadOp: render.LoadOperationClear, + StoreOp: render.StoreOperationStore, + ClearValue: [4]float32{0.0, 0.0, 0.0, 1.0}, + }, + { + LoadOp: render.LoadOperationClear, + StoreOp: render.StoreOperationStore, + ClearValue: [4]float32{0.0, 0.0, 1.0, 0.0}, + }, + { + LoadOp: render.LoadOperationClear, + StoreOp: render.StoreOperationStore, + ClearValue: [4]float32{0.0, 0.0, 0.0, 1.0}, + }, + }, + }) + s.meshRenderer.DiscardRenderItems() + for _, mesh := range ctx.VisibleMeshes { + s.meshRenderer.QueueMeshRenderItems(ctx, mesh, internal.MeshRenderPassTypeGeometry) + } + for _, meshIndex := range ctx.VisibleStaticMeshIndices { + staticMesh := &ctx.Scene.staticMeshes[meshIndex] + s.meshRenderer.QueueStaticMeshRenderItems(ctx, staticMesh, internal.MeshRenderPassTypeGeometry) + } + s.meshRenderer.Render(ctx) + commandBuffer.EndRenderPass() +} + +func (s *GeometryStage) PostRender() { + // Nothing to do here. +} + +func (s *GeometryStage) allocateFramebuffer() { + s.albedoMetallicTexture = s.input.AlbedoMetallicTexture() + s.normalRoughnessTexture = s.input.NormalRoughnessTexture() + s.depthTexture = s.input.DepthTexture() + + s.framebuffer = s.api.CreateFramebuffer(render.FramebufferInfo{ + ColorAttachments: [4]opt.T[render.TextureAttachment]{ + opt.V(render.PlainTextureAttachment(s.albedoMetallicTexture)), + opt.V(render.PlainTextureAttachment(s.normalRoughnessTexture)), + }, + DepthAttachment: opt.V(render.PlainTextureAttachment(s.depthTexture)), + }) +} + +func (s *GeometryStage) releaseFramebuffer() { + defer s.framebuffer.Release() +} diff --git a/game/graphics/stage_geometry_source.go b/game/graphics/stage_geometry_source.go new file mode 100644 index 00000000..a98db4d4 --- /dev/null +++ b/game/graphics/stage_geometry_source.go @@ -0,0 +1,84 @@ +package graphics + +import "github.com/mokiat/lacking/render" + +func newGeometrySourceStage(api render.API) *GeometrySourceStage { + return &GeometrySourceStage{ + api: api, + } +} + +var _ Stage = (*GeometrySourceStage)(nil) + +// GeometrySourceStage is a stage that provides source textures for +// a geometry pass renderer. +type GeometrySourceStage struct { + api render.API + + framebufferWidth uint32 + framebufferHeight uint32 + + albedoMetallicTexture render.Texture + normalRoughnessTexture render.Texture +} + +// AlbedoMetallicTexture returns the texture that contains the albedo +// color in the RGB channels and the metallic factor in the A channel. +func (s *GeometrySourceStage) AlbedoMetallicTexture() render.Texture { + return s.albedoMetallicTexture +} + +// NormalRoughnessTexture returns the texture that contains the normal +// vector in the RGB channels and the roughness factor in the A channel. +func (s *GeometrySourceStage) NormalRoughnessTexture() render.Texture { + return s.normalRoughnessTexture +} + +func (s *GeometrySourceStage) Allocate() { + s.framebufferWidth = 32 + s.framebufferHeight = 32 + s.allocateTextures() +} + +func (s *GeometrySourceStage) Release() { + defer s.releaseTextures() +} + +func (s *GeometrySourceStage) PreRender(width, height uint32) { + if s.framebufferWidth != width || s.framebufferHeight != height { + s.framebufferWidth = width + s.framebufferHeight = height + s.releaseTextures() + s.allocateTextures() + } +} + +func (s *GeometrySourceStage) Render(ctx StageContext) { + // Nothing to do here. +} + +func (s *GeometrySourceStage) PostRender() { + // Nothing to do here. +} + +func (s *GeometrySourceStage) allocateTextures() { + s.albedoMetallicTexture = s.api.CreateColorTexture2D(render.ColorTexture2DInfo{ + Width: s.framebufferWidth, + Height: s.framebufferHeight, + GenerateMipmaps: false, + GammaCorrection: false, + Format: render.DataFormatRGBA8, + }) + s.normalRoughnessTexture = s.api.CreateColorTexture2D(render.ColorTexture2DInfo{ + Width: s.framebufferWidth, + Height: s.framebufferHeight, + GenerateMipmaps: false, + GammaCorrection: false, + Format: render.DataFormatRGBA16F, + }) +} + +func (s *GeometrySourceStage) releaseTextures() { + defer s.albedoMetallicTexture.Release() + defer s.normalRoughnessTexture.Release() +} diff --git a/game/graphics/stage_lighting.go b/game/graphics/stage_lighting.go new file mode 100644 index 00000000..319d6f25 --- /dev/null +++ b/game/graphics/stage_lighting.go @@ -0,0 +1,475 @@ +package graphics + +import ( + "github.com/mokiat/gog/opt" + "github.com/mokiat/gomath/dtos" + "github.com/mokiat/gomath/sprec" + "github.com/mokiat/lacking/debug/metric" + "github.com/mokiat/lacking/game/graphics/internal" + "github.com/mokiat/lacking/render" + "github.com/mokiat/lacking/render/ubo" +) + +// LightingStageInput is the input data for the LightingStage. +type LightingStageInput struct { + AlbedoMetallicTexture StageTextureParameter + NormalRoughnessTexture StageTextureParameter + DepthTexture StageTextureParameter + HDRTexture StageTextureParameter +} + +func newLightingStage(api render.API, shaders ShaderCollection, data *commonStageData, input LightingStageInput) *LightingStage { + return &LightingStage{ + api: api, + shaders: shaders, + data: data, + input: input, + } +} + +var _ Stage = (*LightingStage)(nil) + +// LightingStage is responsible for rendering the lighting of the scene. +type LightingStage struct { + api render.API + shaders ShaderCollection + data *commonStageData + input LightingStageInput + + hdrTexture render.Texture + framebuffer render.Framebuffer + + noShadowTexture render.Texture + + ambientLightProgram render.Program + ambientLightPipeline render.Pipeline + + pointLightProgram render.Program + pointLightPipeline render.Pipeline + + spotLightProgram render.Program + spotLightPipeline render.Pipeline + + directionalLightProgram render.Program + directionalLightPipeline render.Pipeline +} + +func (s *LightingStage) Allocate() { + s.allocateFramebuffer() + s.allocatePipelines() +} + +func (s *LightingStage) Release() { + defer s.releaseFramebuffer() + defer s.releasePipelines() +} + +func (s *LightingStage) PreRender(width, height uint32) { + hdrTexture := s.input.HDRTexture() + if hdrTexture != s.hdrTexture { + s.releaseFramebuffer() + s.allocateFramebuffer() + } +} + +func (s *LightingStage) Render(ctx StageContext) { + defer metric.BeginRegion("lighting").End() + + commandBuffer := ctx.CommandBuffer + commandBuffer.BeginRenderPass(render.RenderPassInfo{ + Framebuffer: s.framebuffer, + Viewport: render.Area{ + Width: s.hdrTexture.Width(), + Height: s.hdrTexture.Height(), + }, + DepthLoadOp: render.LoadOperationLoad, + DepthStoreOp: render.StoreOperationStore, + StencilLoadOp: render.LoadOperationLoad, + StencilStoreOp: render.StoreOperationDiscard, + Colors: [4]render.ColorAttachmentInfo{ + { + LoadOp: render.LoadOperationClear, + StoreOp: render.StoreOperationStore, + ClearValue: [4]float32{0.0, 0.0, 0.0, 1.0}, + }, + }, + }) + + for _, ambientLight := range ctx.VisibleAmbientLights { + if ambientLight.active { + s.renderAmbientLight(ctx, ambientLight) + } + } + for _, pointLight := range ctx.VisiblePointLights { + if pointLight.active { + s.renderPointLight(ctx, pointLight) + } + } + for _, spotLight := range ctx.VisibleSpotLights { + if spotLight.active { + s.renderSpotLight(ctx, spotLight) + } + } + for _, directionalLight := range ctx.VisibleDirectionalLights { + if directionalLight.active { + s.renderDirectionalLight(ctx, directionalLight) + } + } + + commandBuffer.EndRenderPass() +} + +func (s *LightingStage) PostRender() { + // Nothing to do here. +} + +func (s *LightingStage) allocateFramebuffer() { + s.hdrTexture = s.input.HDRTexture() + + s.framebuffer = s.api.CreateFramebuffer(render.FramebufferInfo{ + ColorAttachments: [4]opt.T[render.TextureAttachment]{ + opt.V(render.PlainTextureAttachment(s.hdrTexture)), + }, + }) +} + +func (s *LightingStage) releaseFramebuffer() { + defer s.framebuffer.Release() +} + +func (s *LightingStage) allocatePipelines() { + quadShape := s.data.QuadShape() + sphereShape := s.data.SphereShape() + coneShape := s.data.ConeShape() + + s.noShadowTexture = s.api.CreateDepthTexture2D(render.DepthTexture2DInfo{ + Width: 1, + Height: 1, + Comparable: true, + // TODO: Initialize to furthest possible depth value + }) + + s.ambientLightProgram = s.api.CreateProgram(render.ProgramInfo{ + SourceCode: s.shaders.AmbientLightSet(), + TextureBindings: []render.TextureBinding{ + render.NewTextureBinding("fbColor0TextureIn", internal.TextureBindingLightingFramebufferColor0), + render.NewTextureBinding("fbColor1TextureIn", internal.TextureBindingLightingFramebufferColor1), + render.NewTextureBinding("fbDepthTextureIn", internal.TextureBindingLightingFramebufferDepth), + render.NewTextureBinding("reflectionTextureIn", internal.TextureBindingLightingReflectionTexture), + render.NewTextureBinding("refractionTextureIn", internal.TextureBindingLightingRefractionTexture), + }, + UniformBindings: []render.UniformBinding{ + render.NewUniformBinding("Camera", internal.UniformBufferBindingCamera), + }, + }) + s.ambientLightPipeline = s.api.CreatePipeline(render.PipelineInfo{ + Program: s.ambientLightProgram, + VertexArray: quadShape.VertexArray(), + Topology: quadShape.Topology(), + Culling: render.CullModeBack, + FrontFace: render.FaceOrientationCCW, + DepthTest: false, + DepthWrite: false, + DepthComparison: render.ComparisonAlways, + StencilTest: false, + ColorWrite: render.ColorMaskTrue, + BlendEnabled: true, + BlendColor: [4]float32{0.0, 0.0, 0.0, 0.0}, + BlendSourceColorFactor: render.BlendFactorOne, + BlendSourceAlphaFactor: render.BlendFactorOne, + BlendDestinationColorFactor: render.BlendFactorOne, + BlendDestinationAlphaFactor: render.BlendFactorZero, + BlendOpColor: render.BlendOperationAdd, + BlendOpAlpha: render.BlendOperationAdd, + }) + + s.pointLightProgram = s.api.CreateProgram(render.ProgramInfo{ + SourceCode: s.shaders.PointLightSet(), + TextureBindings: []render.TextureBinding{ + render.NewTextureBinding("fbColor0TextureIn", internal.TextureBindingLightingFramebufferColor0), + render.NewTextureBinding("fbColor1TextureIn", internal.TextureBindingLightingFramebufferColor1), + render.NewTextureBinding("fbDepthTextureIn", internal.TextureBindingLightingFramebufferDepth), + }, + UniformBindings: []render.UniformBinding{ + render.NewUniformBinding("Camera", internal.UniformBufferBindingCamera), + render.NewUniformBinding("Light", internal.UniformBufferBindingLight), + }, + }) + s.pointLightPipeline = s.api.CreatePipeline(render.PipelineInfo{ + Program: s.pointLightProgram, + VertexArray: sphereShape.VertexArray(), + Topology: sphereShape.Topology(), + Culling: render.CullModeFront, + FrontFace: render.FaceOrientationCCW, + DepthTest: false, + DepthWrite: false, + DepthComparison: render.ComparisonAlways, + StencilTest: false, + ColorWrite: render.ColorMaskTrue, + BlendEnabled: true, + BlendColor: [4]float32{0.0, 0.0, 0.0, 0.0}, + BlendSourceColorFactor: render.BlendFactorOne, + BlendSourceAlphaFactor: render.BlendFactorOne, + BlendDestinationColorFactor: render.BlendFactorOne, + BlendDestinationAlphaFactor: render.BlendFactorZero, + BlendOpColor: render.BlendOperationAdd, + BlendOpAlpha: render.BlendOperationAdd, + }) + + s.spotLightProgram = s.api.CreateProgram(render.ProgramInfo{ + SourceCode: s.shaders.SpotLightSet(), + TextureBindings: []render.TextureBinding{ + render.NewTextureBinding("fbColor0TextureIn", internal.TextureBindingLightingFramebufferColor0), + render.NewTextureBinding("fbColor1TextureIn", internal.TextureBindingLightingFramebufferColor1), + render.NewTextureBinding("fbDepthTextureIn", internal.TextureBindingLightingFramebufferDepth), + }, + UniformBindings: []render.UniformBinding{ + render.NewUniformBinding("Camera", internal.UniformBufferBindingCamera), + render.NewUniformBinding("Light", internal.UniformBufferBindingLight), + }, + }) + s.spotLightPipeline = s.api.CreatePipeline(render.PipelineInfo{ + Program: s.spotLightProgram, + VertexArray: coneShape.VertexArray(), + Topology: coneShape.Topology(), + Culling: render.CullModeFront, + FrontFace: render.FaceOrientationCCW, + DepthTest: false, + DepthWrite: false, + DepthComparison: render.ComparisonAlways, + StencilTest: false, + ColorWrite: render.ColorMaskTrue, + BlendEnabled: true, + BlendColor: [4]float32{0.0, 0.0, 0.0, 0.0}, + BlendSourceColorFactor: render.BlendFactorOne, + BlendSourceAlphaFactor: render.BlendFactorOne, + BlendDestinationColorFactor: render.BlendFactorOne, + BlendDestinationAlphaFactor: render.BlendFactorZero, + BlendOpColor: render.BlendOperationAdd, + BlendOpAlpha: render.BlendOperationAdd, + }) + + s.directionalLightProgram = s.api.CreateProgram(render.ProgramInfo{ + SourceCode: s.shaders.DirectionalLightSet(), + TextureBindings: []render.TextureBinding{ + render.NewTextureBinding("fbColor0TextureIn", internal.TextureBindingLightingFramebufferColor0), + render.NewTextureBinding("fbColor1TextureIn", internal.TextureBindingLightingFramebufferColor1), + render.NewTextureBinding("fbDepthTextureIn", internal.TextureBindingLightingFramebufferDepth), + render.NewTextureBinding("lackingShadowMap", internal.TextureBindingLightingShadowMap), + }, + UniformBindings: []render.UniformBinding{ + render.NewUniformBinding("Camera", internal.UniformBufferBindingCamera), + render.NewUniformBinding("Light", internal.UniformBufferBindingLight), + }, + }) + s.directionalLightPipeline = s.api.CreatePipeline(render.PipelineInfo{ + Program: s.directionalLightProgram, + VertexArray: quadShape.VertexArray(), + Topology: quadShape.Topology(), + Culling: render.CullModeBack, + FrontFace: render.FaceOrientationCCW, + DepthTest: false, + DepthWrite: false, + DepthComparison: render.ComparisonAlways, + StencilTest: false, + ColorWrite: render.ColorMaskTrue, + BlendEnabled: true, + BlendColor: [4]float32{0.0, 0.0, 0.0, 0.0}, + BlendSourceColorFactor: render.BlendFactorOne, + BlendSourceAlphaFactor: render.BlendFactorOne, + BlendDestinationColorFactor: render.BlendFactorOne, + BlendDestinationAlphaFactor: render.BlendFactorZero, + BlendOpColor: render.BlendOperationAdd, + BlendOpAlpha: render.BlendOperationAdd, + }) +} + +func (s *LightingStage) releasePipelines() { + defer s.ambientLightProgram.Release() + defer s.ambientLightPipeline.Release() + + defer s.pointLightProgram.Release() + defer s.pointLightPipeline.Release() + + defer s.spotLightProgram.Release() + defer s.spotLightPipeline.Release() + + defer s.directionalLightProgram.Release() + defer s.directionalLightPipeline.Release() +} + +func (s *LightingStage) renderAmbientLight(ctx StageContext, light *AmbientLight) { + quadShape := s.data.QuadShape() + + nearestSampler := s.data.NearestSampler() + linearSampler := s.data.LinearSampler() + + albedoMetallicTexture := s.input.AlbedoMetallicTexture() + normalRoughnessTexture := s.input.NormalRoughnessTexture() + depthTexture := s.input.DepthTexture() + + commandBuffer := ctx.CommandBuffer + // TODO: Ambient light intensity based on distance and inner and outer radius + commandBuffer.BindPipeline(s.ambientLightPipeline) + commandBuffer.TextureUnit(internal.TextureBindingLightingFramebufferColor0, albedoMetallicTexture) + commandBuffer.SamplerUnit(internal.TextureBindingLightingFramebufferColor0, nearestSampler) + commandBuffer.TextureUnit(internal.TextureBindingLightingFramebufferColor1, normalRoughnessTexture) + commandBuffer.SamplerUnit(internal.TextureBindingLightingFramebufferColor1, nearestSampler) + commandBuffer.TextureUnit(internal.TextureBindingLightingFramebufferDepth, depthTexture) + commandBuffer.SamplerUnit(internal.TextureBindingLightingFramebufferDepth, nearestSampler) + commandBuffer.TextureUnit(internal.TextureBindingLightingReflectionTexture, light.reflectionTexture) + commandBuffer.SamplerUnit(internal.TextureBindingLightingReflectionTexture, linearSampler) + commandBuffer.TextureUnit(internal.TextureBindingLightingRefractionTexture, light.refractionTexture) + commandBuffer.SamplerUnit(internal.TextureBindingLightingRefractionTexture, linearSampler) + commandBuffer.UniformBufferUnit( + internal.UniformBufferBindingCamera, + ctx.CameraPlacement.Buffer, + ctx.CameraPlacement.Offset, + ctx.CameraPlacement.Size, + ) + commandBuffer.DrawIndexed(0, quadShape.IndexCount(), 1) +} + +func (s *LightingStage) renderPointLight(ctx StageContext, light *PointLight) { + sphereShape := s.data.SphereShape() + nearestSampler := s.data.NearestSampler() + + commandBuffer := ctx.CommandBuffer + uniformBuffer := ctx.UniformBuffer + + albedoMetallicTexture := s.input.AlbedoMetallicTexture() + normalRoughnessTexture := s.input.NormalRoughnessTexture() + depthTexture := s.input.DepthTexture() + + lightPlacement := ubo.WriteUniform(uniformBuffer, internal.LightUniform{ + ShadowMatrices: [8]sprec.Mat4{}, // irrelevant + ShadowCascades: [8]sprec.Vec2{}, // irrelevant + ModelMatrix: light.gfxMatrix(), + Color: dtos.Vec3(light.emitColor), + Intensity: 1.0, + Range: float32(light.emitRange), + }) + + commandBuffer.BindPipeline(s.pointLightPipeline) + commandBuffer.TextureUnit(internal.TextureBindingLightingFramebufferColor0, albedoMetallicTexture) + commandBuffer.SamplerUnit(internal.TextureBindingLightingFramebufferColor0, nearestSampler) + commandBuffer.TextureUnit(internal.TextureBindingLightingFramebufferColor1, normalRoughnessTexture) + commandBuffer.SamplerUnit(internal.TextureBindingLightingFramebufferColor1, nearestSampler) + commandBuffer.TextureUnit(internal.TextureBindingLightingFramebufferDepth, depthTexture) + commandBuffer.SamplerUnit(internal.TextureBindingLightingFramebufferDepth, nearestSampler) + commandBuffer.UniformBufferUnit( + internal.UniformBufferBindingCamera, + ctx.CameraPlacement.Buffer, + ctx.CameraPlacement.Offset, + ctx.CameraPlacement.Size, + ) + commandBuffer.UniformBufferUnit( + internal.UniformBufferBindingLight, + lightPlacement.Buffer, + lightPlacement.Offset, + lightPlacement.Size, + ) + commandBuffer.DrawIndexed(0, sphereShape.IndexCount(), 1) +} + +func (s *LightingStage) renderSpotLight(ctx StageContext, light *SpotLight) { + coneShape := s.data.ConeShape() + nearestSampler := s.data.NearestSampler() + + uniformBuffer := ctx.UniformBuffer + commandBuffer := ctx.CommandBuffer + + albedoMetallicTexture := s.input.AlbedoMetallicTexture() + normalRoughnessTexture := s.input.NormalRoughnessTexture() + depthTexture := s.input.DepthTexture() + + lightPlacement := ubo.WriteUniform(uniformBuffer, internal.LightUniform{ + ShadowMatrices: [8]sprec.Mat4{}, // irrelevant + ShadowCascades: [8]sprec.Vec2{}, // irrelevant + ModelMatrix: light.gfxMatrix(), + Color: dtos.Vec3(light.emitColor), + Intensity: 1.0, + Range: float32(light.emitRange), + OuterAngle: float32(light.emitOuterConeAngle.Radians()), + InnerAngle: float32(light.emitInnerConeAngle.Radians()), + }) + + commandBuffer.BindPipeline(s.spotLightPipeline) + commandBuffer.TextureUnit(internal.TextureBindingLightingFramebufferColor0, albedoMetallicTexture) + commandBuffer.SamplerUnit(internal.TextureBindingLightingFramebufferColor0, nearestSampler) + commandBuffer.TextureUnit(internal.TextureBindingLightingFramebufferColor1, normalRoughnessTexture) + commandBuffer.SamplerUnit(internal.TextureBindingLightingFramebufferColor1, nearestSampler) + commandBuffer.TextureUnit(internal.TextureBindingLightingFramebufferDepth, depthTexture) + commandBuffer.SamplerUnit(internal.TextureBindingLightingFramebufferDepth, nearestSampler) + commandBuffer.UniformBufferUnit( + internal.UniformBufferBindingCamera, + ctx.CameraPlacement.Buffer, + ctx.CameraPlacement.Offset, + ctx.CameraPlacement.Size, + ) + commandBuffer.UniformBufferUnit( + internal.UniformBufferBindingLight, + lightPlacement.Buffer, + lightPlacement.Offset, + lightPlacement.Size, + ) + commandBuffer.DrawIndexed(0, coneShape.IndexCount(), 1) +} + +func (s *LightingStage) renderDirectionalLight(ctx StageContext, light *DirectionalLight) { + quadShape := s.data.QuadShape() + nearestSampler := s.data.NearestSampler() + depthSampler := s.data.DepthSampler() + + uniformBuffer := ctx.UniformBuffer + commandBuffer := ctx.CommandBuffer + + albedoMetallicTexture := s.input.AlbedoMetallicTexture() + normalRoughnessTexture := s.input.NormalRoughnessTexture() + depthTexture := s.input.DepthTexture() + + lightUniform := internal.LightUniform{ + ShadowMatrices: [8]sprec.Mat4{}, + ModelMatrix: light.gfxMatrix(), + ShadowCascades: [8]sprec.Vec2{}, + Color: dtos.Vec3(light.emitColor), + Intensity: 1.0, + Range: 0.0, // irrelevant + OuterAngle: 0.0, // irrelevant + InnerAngle: 0.0, // irrelevant + } + + shadowTexture := s.noShadowTexture + if shadowMap := s.data.GetDirectionalShadowMap(light); shadowMap != nil { + shadowTexture = shadowMap.ArrayTexture + for i, cascade := range shadowMap.Cascades { + lightUniform.ShadowMatrices[i] = cascade.ProjectionMatrix + lightUniform.ShadowCascades[i] = sprec.NewVec2(cascade.Near, cascade.Far) + } + } + + lightPlacement := ubo.WriteUniform(uniformBuffer, lightUniform) + + commandBuffer.BindPipeline(s.directionalLightPipeline) + commandBuffer.TextureUnit(internal.TextureBindingLightingFramebufferColor0, albedoMetallicTexture) + commandBuffer.SamplerUnit(internal.TextureBindingLightingFramebufferColor0, nearestSampler) + commandBuffer.TextureUnit(internal.TextureBindingLightingFramebufferColor1, normalRoughnessTexture) + commandBuffer.SamplerUnit(internal.TextureBindingLightingFramebufferColor1, nearestSampler) + commandBuffer.TextureUnit(internal.TextureBindingLightingFramebufferDepth, depthTexture) + commandBuffer.SamplerUnit(internal.TextureBindingLightingFramebufferDepth, nearestSampler) + commandBuffer.TextureUnit(internal.TextureBindingLightingShadowMap, shadowTexture) + commandBuffer.SamplerUnit(internal.TextureBindingLightingShadowMap, depthSampler) + commandBuffer.UniformBufferUnit( + internal.UniformBufferBindingCamera, + ctx.CameraPlacement.Buffer, + ctx.CameraPlacement.Offset, + ctx.CameraPlacement.Size, + ) + commandBuffer.UniformBufferUnit( + internal.UniformBufferBindingLight, + lightPlacement.Buffer, + lightPlacement.Offset, + lightPlacement.Size, + ) + commandBuffer.DrawIndexed(0, quadShape.IndexCount(), 1) +} diff --git a/game/graphics/stage_mesh.go b/game/graphics/stage_mesh.go new file mode 100644 index 00000000..36fb37b1 --- /dev/null +++ b/game/graphics/stage_mesh.go @@ -0,0 +1,213 @@ +package graphics + +import ( + "cmp" + "math" + "slices" + + "github.com/mokiat/gblob" + "github.com/mokiat/gomath/dprec" + "github.com/mokiat/lacking/game/graphics/internal" + "github.com/mokiat/lacking/render/ubo" +) + +const ( + initialRenderItemCount = 32 * 1024 + + // TODO: Move these next to the uniform types + modelUniformBufferItemSize = 64 + modelUniformBufferItemCount = 256 + modelUniformBufferSize = modelUniformBufferItemSize * modelUniformBufferItemCount +) + +func newMeshRenderer() *meshRenderer { + return &meshRenderer{ + renderItems: make([]renderItem, 0, initialRenderItemCount), + modelUniformBufferData: make(gblob.LittleEndianBlock, modelUniformBufferSize), + } +} + +type meshRenderer struct { + renderItems []renderItem + modelUniformBufferData gblob.LittleEndianBlock +} + +func (s *meshRenderer) DiscardRenderItems() { + s.renderItems = s.renderItems[:0] +} + +func (s *meshRenderer) QueueMeshRenderItems(ctx StageContext, mesh *Mesh, passType internal.MeshRenderPassType) { + if !mesh.active { + return + } + definition := mesh.definition + passes := definition.passesByType[passType] + for _, pass := range passes { + s.renderItems = append(s.renderItems, renderItem{ + Layer: pass.Layer, + MaterialKey: pass.Key, + ArmatureKey: mesh.armature.key(), + + Pipeline: pass.Pipeline, + TextureSet: pass.TextureSet, + UniformSet: pass.UniformSet, + ModelData: mesh.matrixData, + ArmatureData: mesh.armature.uniformData(), + + IndexByteOffset: pass.IndexByteOffset, + IndexCount: pass.IndexCount, + }) + } +} + +func (s *meshRenderer) QueueStaticMeshRenderItems(ctx StageContext, mesh *StaticMesh, passType internal.MeshRenderPassType) { + if !mesh.active { + return + } + distance := dprec.Vec3Diff(mesh.position, ctx.CameraPosition).Length() + if distance < mesh.minDistance || mesh.maxDistance < distance { + return + } + + // TODO: Extract common stuff between mesh and static mesh into a type + // that is passed ot this function instead so that it can be reused. + definition := mesh.definition + passes := definition.passesByType[passType] + for _, pass := range passes { + s.renderItems = append(s.renderItems, renderItem{ + Layer: pass.Layer, + MaterialKey: pass.Key, + ArmatureKey: math.MaxUint32, + + Pipeline: pass.Pipeline, + TextureSet: pass.TextureSet, + UniformSet: pass.UniformSet, + ModelData: mesh.matrixData, + ArmatureData: nil, + + IndexByteOffset: pass.IndexByteOffset, + IndexCount: pass.IndexCount, + }) + } +} + +func (s *meshRenderer) Render(ctx StageContext) { + s.renderMeshRenderItems(ctx, s.renderItems) + s.renderItems = s.renderItems[:0] +} + +func (s *meshRenderer) renderMeshRenderItems(ctx StageContext, items []renderItem) { + const maxBatchSize = modelUniformBufferItemCount + var ( + lastMaterialKey = uint32(math.MaxUint32) + lastArmatureKey = uint32(math.MaxUint32) + + batchStart = 0 + batchEnd = 0 + ) + + slices.SortFunc(items, compareMeshRenderItems) + + itemCount := len(items) + for i, item := range items { + materialKey := item.MaterialKey + armatureKey := item.ArmatureKey + + isSame := (materialKey == lastMaterialKey) && (armatureKey == lastArmatureKey) + if !isSame { + if batchStart < batchEnd { + s.renderMeshRenderItemBatch(ctx, items[batchStart:batchEnd]) + } + batchStart = batchEnd + } + batchEnd++ + + batchSize := batchEnd - batchStart + if (batchSize >= maxBatchSize) || (i == itemCount-1) { + s.renderMeshRenderItemBatch(ctx, items[batchStart:batchEnd]) + batchStart = batchEnd + } + + lastMaterialKey = materialKey + lastArmatureKey = armatureKey + } +} + +func (s *meshRenderer) renderMeshRenderItemBatch(ctx StageContext, items []renderItem) { + template := items[0] + + commandBuffer := ctx.CommandBuffer + uniformBuffer := ctx.UniformBuffer + + commandBuffer.BindPipeline(template.Pipeline) + + // Camera data is shared between all items. + cameraPlacement := ctx.CameraPlacement + commandBuffer.UniformBufferUnit( + internal.UniformBufferBindingCamera, + cameraPlacement.Buffer, + cameraPlacement.Offset, + cameraPlacement.Size, + ) + + // Material data is shared between all items. + if !template.UniformSet.IsEmpty() { + materialPlacement := ubo.WriteUniform(uniformBuffer, internal.MaterialUniform{ + Data: template.UniformSet.Data(), + }) + commandBuffer.UniformBufferUnit( + internal.UniformBufferBindingMaterial, + materialPlacement.Buffer, + materialPlacement.Offset, + materialPlacement.Size, + ) + } + + for i := range template.TextureSet.TextureCount() { + if texture := template.TextureSet.TextureAt(i); texture != nil { + commandBuffer.TextureUnit(uint(i), texture) + } + if sampler := template.TextureSet.SamplerAt(i); sampler != nil { + commandBuffer.SamplerUnit(uint(i), sampler) + } + } + + // Model data needs to be combined. + for i, item := range items { + start := i * modelUniformBufferItemSize + end := start + modelUniformBufferItemSize + copy(s.modelUniformBufferData[start:end], item.ModelData) + } + modelPlacement := ubo.WriteUniform(uniformBuffer, internal.ModelUniform{ + ModelMatrices: s.modelUniformBufferData, + }) + commandBuffer.UniformBufferUnit( + internal.UniformBufferBindingModel, + modelPlacement.Buffer, + modelPlacement.Offset, + modelPlacement.Size, + ) + + // Armature data is shared between all items. + if template.ArmatureData != nil { + armaturePlacement := ubo.WriteUniform(uniformBuffer, internal.ArmatureUniform{ + BoneMatrices: template.ArmatureData, + }) + commandBuffer.UniformBufferUnit( + internal.UniformBufferBindingArmature, + armaturePlacement.Buffer, + armaturePlacement.Offset, + armaturePlacement.Size, + ) + } + + commandBuffer.DrawIndexed(template.IndexByteOffset, template.IndexCount, uint32(len(items))) +} + +func compareMeshRenderItems(a, b renderItem) int { + return cmp.Or( + cmp.Compare(a.Layer, b.Layer), + cmp.Compare(a.MaterialKey, b.MaterialKey), + cmp.Compare(a.ArmatureKey, b.ArmatureKey), + ) +} diff --git a/game/graphics/stage_probe.go b/game/graphics/stage_probe.go new file mode 100644 index 00000000..bcab1ea2 --- /dev/null +++ b/game/graphics/stage_probe.go @@ -0,0 +1,204 @@ +package graphics + +import ( + "time" + + "github.com/mokiat/gblob" + "github.com/mokiat/gog/opt" + "github.com/mokiat/gomath/sprec" + "github.com/mokiat/lacking/debug/metric" + "github.com/mokiat/lacking/game/graphics/internal" + "github.com/mokiat/lacking/render" + "github.com/x448/float16" +) + +// ExposureProbeStageInput is used to configure an ExposureProbeStage. +type ExposureProbeStageInput struct { + HDRTexture StageTextureParameter +} + +func newExposureProbeStage(api render.API, shaders ShaderCollection, data *commonStageData, input ExposureProbeStageInput) *ExposureProbeStage { + return &ExposureProbeStage{ + api: api, + shaders: shaders, + data: data, + + hdrTexture: input.HDRTexture, + + exposureBufferData: make([]byte, 4*render.SizeF32), // worst case RGBA32F + exposureTarget: 1.0, + } +} + +var _ Stage = (*ExposureProbeStage)(nil) + +// ExposureProbeStage is a stage that measures the brightness of the scene +// and adjusts the exposure of the camera accordingly. +type ExposureProbeStage struct { + api render.API + shaders ShaderCollection + data *commonStageData + + hdrTexture StageTextureParameter + + exposureAlbedoTexture render.Texture + exposureFramebuffer render.Framebuffer + exposureFormat render.DataFormat + exposureProgram render.Program + exposurePipeline render.Pipeline + exposureBufferData gblob.LittleEndianBlock + exposureBuffer render.Buffer + exposureSync render.Fence + exposureSyncNeeded bool + exposureTarget float32 + exposureUpdateTimestamp time.Time +} + +func (s *ExposureProbeStage) Allocate() { + quadShape := s.data.QuadShape() + + s.exposureAlbedoTexture = s.api.CreateColorTexture2D(render.ColorTexture2DInfo{ + Width: 1, + Height: 1, + GenerateMipmaps: false, + GammaCorrection: false, + Format: render.DataFormatRGBA16F, + }) + s.exposureFramebuffer = s.api.CreateFramebuffer(render.FramebufferInfo{ + ColorAttachments: [4]opt.T[render.TextureAttachment]{ + opt.V(render.PlainTextureAttachment(s.exposureAlbedoTexture)), + }, + }) + + s.exposureProgram = s.api.CreateProgram(render.ProgramInfo{ + SourceCode: s.shaders.ExposureSet(), + TextureBindings: []render.TextureBinding{ + render.NewTextureBinding("fbColor0TextureIn", internal.TextureBindingLightingFramebufferColor0), + }, + UniformBindings: []render.UniformBinding{}, + }) + s.exposurePipeline = s.api.CreatePipeline(render.PipelineInfo{ + Program: s.exposureProgram, + VertexArray: quadShape.VertexArray(), + Topology: quadShape.Topology(), + Culling: render.CullModeBack, + FrontFace: render.FaceOrientationCCW, + DepthTest: false, + DepthWrite: false, + StencilTest: false, + ColorWrite: render.ColorMaskTrue, + BlendEnabled: false, + }) + + s.exposureBuffer = s.api.CreatePixelTransferBuffer(render.BufferInfo{ + Dynamic: true, + Size: uint32(len(s.exposureBufferData)), + }) + s.exposureFormat = s.api.DetermineContentFormat(s.exposureFramebuffer) + if s.exposureFormat == render.DataFormatUnsupported { + // This happens on MacOS on native; fallback to a default format and + // hope for the best. + s.exposureFormat = render.DataFormatRGBA32F + } +} + +func (s *ExposureProbeStage) Release() { + defer s.exposureAlbedoTexture.Release() + defer s.exposureFramebuffer.Release() + + defer s.exposureProgram.Release() + defer s.exposurePipeline.Release() + + defer s.exposureBuffer.Release() +} + +func (s *ExposureProbeStage) PreRender(width, height uint32) { + // Nothing to do here. +} + +func (s *ExposureProbeStage) Render(ctx StageContext) { + if !ctx.Camera.AutoExposure() { + return + } + + defer metric.BeginRegion("exposure").End() + + if s.exposureSync != nil && s.exposureSync.Status() == render.FenceStatusSuccess { + s.api.Queue().ReadBuffer(s.exposureBuffer, 0, s.exposureBufferData) + + var brightness float32 + switch s.exposureFormat { + case render.DataFormatRGBA16F: + brightness = float16.Frombits(s.exposureBufferData.Uint16(0)).Float32() + case render.DataFormatRGBA32F: + brightness = s.exposureBufferData.Float32(0) + } + brightness = sprec.Clamp(brightness, 0.001, 1000.0) + + s.exposureTarget = 1.0 / (2 * 3.14 * brightness) + s.exposureTarget = sprec.Clamp(s.exposureTarget, ctx.Camera.MinimumExposure(), ctx.Camera.MaximumExposure()) + + s.exposureSync.Release() + s.exposureSync = nil + } + + if !s.exposureUpdateTimestamp.IsZero() { + elapsedSeconds := float32(time.Since(s.exposureUpdateTimestamp).Seconds()) + currentExposure := ctx.Camera.Exposure() + alpha := sprec.Clamp(ctx.Camera.AutoExposureSpeed()*elapsedSeconds, 0.0, 1.0) + ctx.Camera.SetExposure(sprec.Mix(currentExposure, s.exposureTarget, alpha)) + } + s.exposureUpdateTimestamp = time.Now() + + if s.exposureSync == nil { + s.exposureSyncNeeded = true + quadShape := s.data.QuadShape() + nearestSampler := s.data.NearestSampler() + commandBuffer := ctx.CommandBuffer + commandBuffer.BeginRenderPass(render.RenderPassInfo{ + Framebuffer: s.exposureFramebuffer, + Viewport: render.Area{ + X: 0, + Y: 0, + Width: 1, + Height: 1, + }, + DepthLoadOp: render.LoadOperationLoad, + DepthStoreOp: render.StoreOperationDiscard, + StencilLoadOp: render.LoadOperationLoad, + StencilStoreOp: render.StoreOperationDiscard, + Colors: [4]render.ColorAttachmentInfo{ + { + LoadOp: render.LoadOperationClear, + StoreOp: render.StoreOperationDiscard, + ClearValue: [4]float32{0.0, 0.0, 0.0, 0.0}, + }, + }, + }) + commandBuffer.BindPipeline(s.exposurePipeline) + commandBuffer.TextureUnit(internal.TextureBindingLightingFramebufferColor0, s.hdrTexture()) + commandBuffer.SamplerUnit(internal.TextureBindingLightingFramebufferColor0, nearestSampler) + commandBuffer.UniformBufferUnit( + internal.UniformBufferBindingCamera, + ctx.CameraPlacement.Buffer, + ctx.CameraPlacement.Offset, + ctx.CameraPlacement.Size, + ) + commandBuffer.DrawIndexed(0, quadShape.IndexCount(), 1) + commandBuffer.CopyFramebufferToBuffer(render.CopyFramebufferToBufferInfo{ + Buffer: s.exposureBuffer, + X: 0, + Y: 0, + Width: 1, + Height: 1, + Format: s.exposureFormat, + }) + commandBuffer.EndRenderPass() + } +} + +func (s *ExposureProbeStage) PostRender() { + if s.exposureSyncNeeded && s.exposureSync == nil { + s.exposureSync = s.api.Queue().TrackSubmittedWorkDone() + } +} diff --git a/game/graphics/stage_provider.go b/game/graphics/stage_provider.go new file mode 100644 index 00000000..fd4612fa --- /dev/null +++ b/game/graphics/stage_provider.go @@ -0,0 +1,74 @@ +package graphics + +import "github.com/mokiat/lacking/render" + +func newStageProvider(api render.API, shaders ShaderCollection, data *commonStageData, meshRenderer *meshRenderer) *StageProvider { + return &StageProvider{ + api: api, + shaders: shaders, + data: data, + meshRenderer: meshRenderer, + } +} + +type StageProvider struct { + api render.API + shaders ShaderCollection + data *commonStageData + meshRenderer *meshRenderer +} + +// CreateDepthSourceStage creates a new DepthSourceStage. +func (p *StageProvider) CreateDepthSourceStage() *DepthSourceStage { + return newDepthSourceStage(p.api) +} + +// CreateGeometrySourceStage creates a new GeometrySourceStage. +func (p *StageProvider) CreateGeometrySourceStage() *GeometrySourceStage { + return newGeometrySourceStage(p.api) +} + +// CreateForwardSourceStage creates a new ForwardSourceStage. +func (p *StageProvider) CreateForwardSourceStage() *ForwardSourceStage { + return newForwardSourceStage(p.api) +} + +// CreateShadowStage creates a new ShadowStage using the specified input object. +func (p *StageProvider) CreateShadowStage() *ShadowStage { + return newShadowStage(p.data, p.meshRenderer) +} + +// CreateGeometryStage creates a new GeometryStage using the specified input +// object. +func (p *StageProvider) CreateGeometryStage(input GeometryStageInput) *GeometryStage { + return newGeometryStage(p.api, p.meshRenderer, input) +} + +// CreateLightingStage creates a new LightingStage using the specified input +// object. +func (p *StageProvider) CreateLightingStage(input LightingStageInput) *LightingStage { + return newLightingStage(p.api, p.shaders, p.data, input) +} + +// CreateForwardStage creates a new ForwardStage using the specified input +// object. +func (p *StageProvider) CreateForwardStage(input ForwardStageInput) *ForwardStage { + return newForwardStage(p.api, p.shaders, p.data, p.meshRenderer, input) +} + +// CreateExposureProbeStage creates a new ExposureProbeStage using the specified +// input object. +func (p *StageProvider) CreateExposureProbeStage(input ExposureProbeStageInput) *ExposureProbeStage { + return newExposureProbeStage(p.api, p.shaders, p.data, input) +} + +// CreateBloomStage creates a new BloomStage using the specified input object. +func (p *StageProvider) CreateBloomStage(input BloomStageInput) *BloomStage { + return newBloomStage(p.api, p.shaders, p.data, input) +} + +// CreateToneMappingStage creates a new ToneMappingStage using the specified +// input object. +func (p *StageProvider) CreateToneMappingStage(input ToneMappingStageInput) *ToneMappingStage { + return newToneMappingStage(p.api, p.shaders, p.data, input) +} diff --git a/game/graphics/stage_shadow.go b/game/graphics/stage_shadow.go new file mode 100644 index 00000000..1cd6db6d --- /dev/null +++ b/game/graphics/stage_shadow.go @@ -0,0 +1,289 @@ +package graphics + +import ( + "cmp" + "slices" + + "github.com/mokiat/gog" + "github.com/mokiat/gomath/dprec" + "github.com/mokiat/gomath/sprec" + "github.com/mokiat/gomath/stod" + "github.com/mokiat/lacking/debug/metric" + "github.com/mokiat/lacking/game/graphics/internal" + "github.com/mokiat/lacking/render" + "github.com/mokiat/lacking/render/ubo" + "github.com/mokiat/lacking/util/spatial" +) + +func newShadowStage(data *commonStageData, meshRenderer *meshRenderer) *ShadowStage { + return &ShadowStage{ + data: data, + meshRenderer: meshRenderer, + + litStaticMeshes: spatial.NewVisitorBucket[uint32](65536), + litMeshes: spatial.NewVisitorBucket[*Mesh](1024), + } +} + +var _ Stage = (*ShadowStage)(nil) + +// ShadowStage is a stage that renders shadows. +type ShadowStage struct { + data *commonStageData + meshRenderer *meshRenderer + + litStaticMeshes *spatial.VisitorBucket[uint32] + litMeshes *spatial.VisitorBucket[*Mesh] +} + +func (s *ShadowStage) Allocate() { + // Nothing to do here. +} + +func (s *ShadowStage) Release() { + // Nothing to do here. +} + +func (s *ShadowStage) PreRender(width, height uint32) { + // Nothing to do here. +} + +func (s *ShadowStage) Render(ctx StageContext) { + defer metric.BeginRegion("shadow").End() + + s.sortLights(ctx) + s.distributeCascadeShadowMaps(ctx) + + for _, light := range ctx.VisibleDirectionalLights { + if !light.active { // TODO: Move this to VisitorBucket closure with iterator. + continue + } + if !light.castShadow { + continue + } + s.renderDirectionalLightShadowMaps(ctx, light) + } +} + +func (s *ShadowStage) PostRender() { + // Nothing to do here. +} + +func (s *ShadowStage) sortLights(ctx StageContext) { + slices.SortFunc(ctx.VisibleAmbientLights, func(a, b *AmbientLight) int { + distanceA := dprec.Vec3Diff(a.Position(), ctx.CameraPosition).Length() + distanceB := dprec.Vec3Diff(b.Position(), ctx.CameraPosition).Length() + return cmp.Compare(distanceA, distanceB) + }) + slices.SortFunc(ctx.VisiblePointLights, func(a, b *PointLight) int { + distanceA := dprec.Vec3Diff(a.Position(), ctx.CameraPosition).Length() + distanceB := dprec.Vec3Diff(b.Position(), ctx.CameraPosition).Length() + return cmp.Compare(distanceA, distanceB) + }) + slices.SortFunc(ctx.VisibleSpotLights, func(a, b *SpotLight) int { + distanceA := dprec.Vec3Diff(a.Position(), ctx.CameraPosition).Length() + distanceB := dprec.Vec3Diff(b.Position(), ctx.CameraPosition).Length() + return cmp.Compare(distanceA, distanceB) + }) + slices.SortFunc(ctx.VisibleDirectionalLights, func(a, b *DirectionalLight) int { + distanceA := dprec.Vec3Diff(a.Position(), ctx.CameraPosition).Length() + distanceB := dprec.Vec3Diff(b.Position(), ctx.CameraPosition).Length() + return cmp.Compare(distanceA, distanceB) + }) +} + +func (s *ShadowStage) distributeCascadeShadowMaps(ctx StageContext) { + s.data.ResetDirectionalShadowMapAssignments() + + for _, light := range ctx.VisibleDirectionalLights { + if !light.active { // TODO: Move this to VisitorBucket closure with iterator. + continue + } + if !light.castShadow { + continue + } + + shadowMap := s.data.AssignDirectionalShadowMap(light) + if shadowMap == nil { + return // no more free shadow maps + } + + lightMatrix := light.gfxMatrix() + lightXAxis := lightMatrix.OrientationX() + lightYAxis := lightMatrix.OrientationY() + lightZAxis := lightMatrix.OrientationZ() + shadowView := sprec.InverseMat4(sprec.OrientationMat4(lightXAxis, lightYAxis, lightZAxis)) + + cascadeCount := min(len(ctx.Camera.cascadeDistances), len(shadowMap.Cascades)) + gog.MutateIndex(shadowMap.Cascades[:cascadeCount], func(j int, cascade *internal.DirectionalShadowMapCascade) { + cascadeNear := ctx.Camera.CascadeNear(j) + cascadeFar := ctx.Camera.CascadeFar(j) + cameraProjectionMatrix := cascadeProjectionMatrix(ctx, cascadeNear, cascadeFar) + + cameraModelMatrix := ctx.Camera.gfxMatrix() + cameraViewMatrix := sprec.InverseMat4(cameraModelMatrix) + cameraProjectionViewMatrix := sprec.Mat4Prod(cameraProjectionMatrix, cameraViewMatrix) + + frustumCornerPoints := calculateFrustumCornerPoints(cameraProjectionViewMatrix) + frustumCentralPoint := calculateFrustumCentralPoint(frustumCornerPoints) + frustumRadius := sprec.Ceil(calculateFrustumRadius(frustumCentralPoint, frustumCornerPoints)) + + frustumOffsetX := sprec.Vec3Dot(lightXAxis, frustumCentralPoint) + frustumOffsetY := sprec.Vec3Dot(lightYAxis, frustumCentralPoint) + frustumOffsetZ := sprec.Vec3Dot(lightZAxis, frustumCentralPoint) + + // TODO: make these configurable or based on visible objects in + // view frustum. + shadowNearOverflow := float32(200.0) + shadowFarOverflow := float32(100.0) + + shadowNear := -frustumOffsetZ - (frustumRadius + shadowNearOverflow) + shadowFar := -frustumOffsetZ + (frustumRadius + shadowFarOverflow) + shadowLeft := frustumOffsetX - frustumRadius + shadowRight := frustumOffsetX + frustumRadius + shadowBottom := frustumOffsetY - frustumRadius + shadowTop := frustumOffsetY + frustumRadius + + shadowOrtho := sprec.OrthoMat4(shadowLeft, shadowRight, shadowTop, shadowBottom, shadowNear, shadowFar) + shadowMatrix := sprec.Mat4Prod(shadowOrtho, shadowView) + + // NOTE: If this ever has precision problems, consider moving the + // anchor point on fixed intervals. If that also fails, consider + // using double precision for the error calculation. + anchorPointVec4 := sprec.NewVec4(0.0, 0.0, 0.0, 1.0) + projectedAnchorPoint4 := sprec.Mat4Vec4Prod(shadowMatrix, anchorPointVec4) + projectedAnchorPoint := sprec.Vec3Quot(projectedAnchorPoint4.VecXYZ(), projectedAnchorPoint4.W) + + shadowMapWidth := float32(shadowMap.ArrayTexture.Width()) + shadowMapHeight := float32(shadowMap.ArrayTexture.Height()) + + projectedAnchorPoint.X = (projectedAnchorPoint.X + 1.0) * 0.5 * shadowMapWidth + projectedAnchorPoint.Y = (projectedAnchorPoint.Y + 1.0) * 0.5 * shadowMapHeight + + diffX := projectedAnchorPoint.X - sprec.Floor(projectedAnchorPoint.X) + diffY := projectedAnchorPoint.Y - sprec.Floor(projectedAnchorPoint.Y) + + errorX := diffX * (shadowRight - shadowLeft) / shadowMapWidth + errorY := diffY * (shadowTop - shadowBottom) / shadowMapHeight + + shadowLeft += errorX + shadowRight += errorX + shadowBottom += errorY + shadowTop += errorY + + shadowOrtho = sprec.OrthoMat4(shadowLeft, shadowRight, shadowTop, shadowBottom, shadowNear, shadowFar) + shadowMatrix = sprec.Mat4Prod(shadowOrtho, shadowView) + + cascade.Near = cascadeNear + cascade.Far = cascadeFar + cascade.ProjectionMatrix = shadowMatrix + }) + } +} + +func (s *ShadowStage) renderDirectionalLightShadowMaps(ctx StageContext, light *DirectionalLight) { + shadowMap := s.data.GetDirectionalShadowMap(light) + if shadowMap == nil { + return + } + + cascadeCount := min(len(ctx.Camera.cascadeDistances), len(shadowMap.Cascades)) + for _, cascade := range shadowMap.Cascades[:cascadeCount] { + frustum := spatial.ProjectionRegion(stod.Mat4(cascade.ProjectionMatrix)) + + s.litStaticMeshes.Reset() + ctx.Scene.staticMeshOctree.VisitHexahedronRegion(&frustum, s.litStaticMeshes) + + s.litMeshes.Reset() + ctx.Scene.dynamicMeshSet.VisitHexahedronRegion(&frustum, s.litMeshes) + + s.meshRenderer.DiscardRenderItems() + for _, meshIndex := range s.litStaticMeshes.Items() { + staticMesh := &ctx.Scene.staticMeshes[meshIndex] + s.meshRenderer.QueueStaticMeshRenderItems(ctx, staticMesh, internal.MeshRenderPassTypeShadow) + } + for _, mesh := range s.litMeshes.Items() { + s.meshRenderer.QueueMeshRenderItems(ctx, mesh, internal.MeshRenderPassTypeShadow) + } + + commandBuffer := ctx.CommandBuffer + uniformBuffer := ctx.UniformBuffer + shadowTexture := shadowMap.ArrayTexture + + commandBuffer.BeginRenderPass(render.RenderPassInfo{ + Framebuffer: cascade.Framebuffer, + Viewport: render.Area{ + Width: shadowTexture.Width(), + Height: shadowTexture.Height(), + }, + DepthLoadOp: render.LoadOperationClear, + DepthStoreOp: render.StoreOperationStore, + DepthClearValue: 1.0, + // DepthBias: 64 * 1024.0, + DepthSlopeBias: 2.0, + StencilLoadOp: render.LoadOperationLoad, + StencilStoreOp: render.StoreOperationDiscard, + }) + lightCameraPlacement := ubo.WriteUniform(uniformBuffer, internal.CameraUniform{ + ProjectionMatrix: cascade.ProjectionMatrix, + ViewMatrix: sprec.IdentityMat4(), // irrelevant + CameraMatrix: sprec.IdentityMat4(), // irrelevant + Viewport: sprec.ZeroVec4(), // TODO? + Time: ctx.Scene.Time(), // FIXME? + }) + ctx.CameraPlacement = lightCameraPlacement // FIXME: DIRTY HACK; Use own meshRenderer context + s.meshRenderer.Render(ctx) + commandBuffer.EndRenderPass() + } +} + +func cascadeProjectionMatrix(ctx StageContext, near, far float32) sprec.Mat4 { + oldNear := ctx.Camera.near + oldFar := ctx.Camera.far + + ctx.Camera.near = near + ctx.Camera.far = far + + cameraProjectionMatrix := evaluateProjectionMatrix(ctx.Camera, ctx.Viewport.Width, ctx.Viewport.Height) + + ctx.Camera.near = oldNear + ctx.Camera.far = oldFar + + return cameraProjectionMatrix +} + +func calculateFrustumCornerPoints(projectionMatrix sprec.Mat4) [8]sprec.Vec3 { + inverseProjectionMatrix := sprec.InverseMat4(projectionMatrix) + frustumCornerPoints4D := [8]sprec.Vec4{ + sprec.Mat4Vec4Prod(inverseProjectionMatrix, sprec.NewVec4(-1, -1, -1, 1.0)), + sprec.Mat4Vec4Prod(inverseProjectionMatrix, sprec.NewVec4(1, -1, -1, 1.0)), + sprec.Mat4Vec4Prod(inverseProjectionMatrix, sprec.NewVec4(-1, 1, -1, 1.0)), + sprec.Mat4Vec4Prod(inverseProjectionMatrix, sprec.NewVec4(1, 1, -1, 1.0)), + sprec.Mat4Vec4Prod(inverseProjectionMatrix, sprec.NewVec4(-1, -1, 1, 1.0)), + sprec.Mat4Vec4Prod(inverseProjectionMatrix, sprec.NewVec4(1, -1, 1, 1.0)), + sprec.Mat4Vec4Prod(inverseProjectionMatrix, sprec.NewVec4(-1, 1, 1, 1.0)), + sprec.Mat4Vec4Prod(inverseProjectionMatrix, sprec.NewVec4(1, 1, 1, 1.0)), + } + var frustumCornerPoints [8]sprec.Vec3 + for i, point := range frustumCornerPoints4D { + frustumCornerPoints[i] = sprec.Vec3Quot(point.VecXYZ(), point.W) + } + return frustumCornerPoints +} + +func calculateFrustumCentralPoint(cornerPoints [8]sprec.Vec3) sprec.Vec3 { + var result sprec.Vec3 + for _, point := range cornerPoints { + result = sprec.Vec3Sum(result, point) + } + return sprec.Vec3Quot(result, 8.0) +} + +func calculateFrustumRadius(centralPoint sprec.Vec3, cornerPoints [8]sprec.Vec3) float32 { + var result float32 + for _, point := range cornerPoints { + distance := sprec.Vec3Diff(point, centralPoint).Length() + result = max(result, distance) + } + return result +} diff --git a/game/graphics/stage_tonemap.go b/game/graphics/stage_tonemap.go new file mode 100644 index 00000000..9050163a --- /dev/null +++ b/game/graphics/stage_tonemap.go @@ -0,0 +1,148 @@ +package graphics + +import ( + "github.com/mokiat/gog/opt" + "github.com/mokiat/lacking/debug/metric" + "github.com/mokiat/lacking/game/graphics/internal" + "github.com/mokiat/lacking/render" + "github.com/mokiat/lacking/render/ubo" +) + +type ToneMappingStageInput struct { + HDRTexture StageTextureParameter + BloomTexture opt.T[StageTextureParameter] +} + +func newToneMappingStage(api render.API, shaders ShaderCollection, data *commonStageData, input ToneMappingStageInput) *ToneMappingStage { + return &ToneMappingStage{ + api: api, + shaders: shaders, + data: data, + + toneMapping: ExponentialToneMapping, + + inHDRTexture: input.HDRTexture, + inBloomTexture: input.BloomTexture.ValueOrDefault(nil), + } +} + +var _ Stage = (*ToneMappingStage)(nil) + +// ToneMappingStage is a stage that applies tone mapping to the input +// HDR texture and outputs the result to the framebuffer. +type ToneMappingStage struct { + api render.API + shaders ShaderCollection + data *commonStageData + + program render.Program + pipeline render.Pipeline + + toneMapping ToneMapping + + inHDRTexture StageTextureParameter + inBloomTexture StageTextureParameter +} + +// ToneMapping represents the tone mapping algorithm that should be used +// when rendering the output of the stage. +func (s *ToneMappingStage) ToneMapping() ToneMapping { + return s.toneMapping +} + +// SetToneMapping sets the tone mapping algorithm that should be used +// when rendering the output of the stage. +func (s *ToneMappingStage) SetToneMapping(toneMapping ToneMapping) { + s.toneMapping = toneMapping +} + +func (s *ToneMappingStage) Allocate() { + quadShape := s.data.QuadShape() + + s.program = s.api.CreateProgram(render.ProgramInfo{ + SourceCode: s.shaders.PostprocessingSet(PostprocessingShaderConfig{ + ToneMapping: s.toneMapping, + Bloom: s.inBloomTexture != nil, + }), + TextureBindings: []render.TextureBinding{ + render.NewTextureBinding("fbColor0TextureIn", internal.TextureBindingPostprocessFramebufferColor0), + render.NewTextureBinding("lackingBloomTexture", internal.TextureBindingPostprocessBloom), + }, + UniformBindings: []render.UniformBinding{ + render.NewUniformBinding("Postprocess", internal.UniformBufferBindingPostprocess), + }, + }) + s.pipeline = s.api.CreatePipeline(render.PipelineInfo{ + Program: s.program, + VertexArray: quadShape.VertexArray(), + Topology: quadShape.Topology(), + Culling: render.CullModeBack, + FrontFace: render.FaceOrientationCCW, + DepthTest: false, + DepthWrite: false, + DepthComparison: render.ComparisonAlways, + StencilTest: false, + ColorWrite: [4]bool{true, true, true, true}, + BlendEnabled: false, + }) +} + +func (s *ToneMappingStage) Release() { + defer s.program.Release() + defer s.pipeline.Release() +} + +func (s *ToneMappingStage) PreRender(width, height uint32) { + // Nothing to do here. +} + +func (s *ToneMappingStage) Render(ctx StageContext) { + defer metric.BeginRegion("tonemap").End() + + quadShape := s.data.QuadShape() + nearestSampler := s.data.NearestSampler() + linearSampler := s.data.LinearSampler() + + commandBuffer := ctx.CommandBuffer + uniformBuffer := ctx.UniformBuffer + + postprocessPlacement := ubo.WriteUniform(uniformBuffer, internal.PostprocessUniform{ + Exposure: ctx.Camera.Exposure(), + }) + + commandBuffer.BeginRenderPass(render.RenderPassInfo{ + Framebuffer: ctx.Framebuffer, + Viewport: ctx.Viewport, + DepthLoadOp: render.LoadOperationLoad, + DepthStoreOp: render.StoreOperationDiscard, + StencilLoadOp: render.LoadOperationLoad, + StencilStoreOp: render.StoreOperationDiscard, + Colors: [4]render.ColorAttachmentInfo{ + { + LoadOp: render.LoadOperationLoad, + StoreOp: render.StoreOperationStore, + }, + }, + }) + + commandBuffer.BindPipeline(s.pipeline) + commandBuffer.TextureUnit(internal.TextureBindingPostprocessFramebufferColor0, s.inHDRTexture()) + commandBuffer.SamplerUnit(internal.TextureBindingPostprocessFramebufferColor0, nearestSampler) + if s.inBloomTexture != nil { + commandBuffer.TextureUnit(internal.TextureBindingPostprocessBloom, s.inBloomTexture()) + commandBuffer.SamplerUnit(internal.TextureBindingPostprocessBloom, linearSampler) + } + commandBuffer.UniformBufferUnit( + internal.UniformBufferBindingPostprocess, + postprocessPlacement.Buffer, + postprocessPlacement.Offset, + postprocessPlacement.Size, + ) + commandBuffer.DrawIndexed(0, quadShape.IndexCount(), 1) + + commandBuffer.EndRenderPass() +} + +func (s *ToneMappingStage) PostRender() { + // Nothing to do here. +} diff --git a/game/hierarchy/node.go b/game/hierarchy/node.go index 2f46f2ec..43c82783 100644 --- a/game/hierarchy/node.go +++ b/game/hierarchy/node.go @@ -103,8 +103,11 @@ func (n *Node) Detach() { // // The node can be reused after deletion. func (n *Node) Delete() { - for child := n.firstChild; child != nil; child = child.rightSibling { + child := n.FirstChild() + for child != nil { + next := child.RightSibling() child.Delete() + child = next } if n.source != nil { n.source.Release() diff --git a/game/physics/engine.go b/game/physics/engine.go index 25576434..0caa35b7 100644 --- a/game/physics/engine.go +++ b/game/physics/engine.go @@ -7,9 +7,15 @@ import ( ) // NewEngine creates a new physics engine. -func NewEngine(timestep time.Duration) *Engine { +func NewEngine(opts ...Option) *Engine { + cfg := config{ + Timestep: 16 * time.Millisecond, + } + for _, opt := range opts { + opt(&cfg) + } return &Engine{ - timestep: timestep, + timestep: cfg.Timestep, } } diff --git a/game/physics/option.go b/game/physics/option.go new file mode 100644 index 00000000..400a2e79 --- /dev/null +++ b/game/physics/option.go @@ -0,0 +1,18 @@ +package physics + +import "time" + +// Option is a configuration function that can be used to customize the +// behavior of a physics engine. +type Option func(c *config) + +// WithTimestep configures the physics engine to use the provided timestep. +func WithTimestep(timestep time.Duration) Option { + return func(c *config) { + c.Timestep = timestep + } +} + +type config struct { + Timestep time.Duration +} diff --git a/game/preset/camera.go b/game/preset/camera.go index 1dc090e5..dd7926d1 100644 --- a/game/preset/camera.go +++ b/game/preset/camera.go @@ -213,6 +213,10 @@ func (s *YawPitchCameraSystem) updateGamepad(elapsedSeconds float64, gamepad app deltaTranslation = dprec.Vec3Prod(deltaTranslation, s.translationSpeed*elapsedSeconds) deltaTranslation = dprec.QuatVec3Rotation(yawRotation, deltaTranslation) + if gamepad.LeftBumper() { + deltaTranslation = dprec.Vec3Prod(deltaTranslation, 4.0) + } + nodeComp.Node.SetAbsoluteMatrix(dprec.TRSMat4( dprec.Vec3Sum(oldTranslation, deltaTranslation), dprec.QuatProd(yawRotation, pitchRotation), diff --git a/game/scene.go b/game/scene.go index f7b6a2bb..1c5a5041 100644 --- a/game/scene.go +++ b/game/scene.go @@ -559,7 +559,6 @@ func (s *Scene) placeDirectionalLight(data placementData, instance directionalLi Position: dprec.ZeroVec3(), Rotation: dprec.IdentityQuat(), EmitColor: instance.emitColor, - EmitRange: 25000.0, CastShadow: instance.castShadow, }) node.SetTarget(DirectionalLightNodeTarget{ diff --git a/go.mod b/go.mod index 22b80b76..7f3b8fe5 100644 --- a/go.mod +++ b/go.mod @@ -9,12 +9,12 @@ require ( github.com/mokiat/goexr v0.1.0 github.com/mokiat/gog v0.13.1 github.com/mokiat/gomath v0.9.0 - github.com/onsi/ginkgo/v2 v2.20.0 - github.com/onsi/gomega v1.34.1 - github.com/qmuntal/gltf v0.26.0 + github.com/onsi/ginkgo/v2 v2.20.2 + github.com/onsi/gomega v1.34.2 + github.com/qmuntal/gltf v0.27.0 github.com/x448/float16 v0.8.4 - golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa - golang.org/x/image v0.19.0 + golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e + golang.org/x/image v0.20.0 golang.org/x/sync v0.8.0 ) @@ -22,13 +22,13 @@ require ( github.com/go-logr/logr v1.4.2 // indirect github.com/go-task/slim-sprig/v3 v3.0.0 // indirect github.com/google/go-cmp v0.6.0 // indirect - github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8 // indirect + github.com/google/pprof v0.0.0-20240903155634-a8630aee4ab9 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/rogpeppe/go-internal v1.12.0 // indirect - golang.org/x/net v0.28.0 // indirect - golang.org/x/sys v0.24.0 // indirect - golang.org/x/text v0.17.0 // indirect + golang.org/x/net v0.29.0 // indirect + golang.org/x/sys v0.25.0 // indirect + golang.org/x/text v0.18.0 // indirect golang.org/x/tools v0.24.0 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index 645fc351..5adaf113 100644 --- a/go.sum +++ b/go.sum @@ -9,8 +9,8 @@ github.com/go-test/deep v1.0.1 h1:UQhStjbkDClarlmv0am7OXXO4/GaPdCGiUiMTvi28sg= github.com/go-test/deep v1.0.1/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8 h1:FKHo8hFI3A+7w0aUQuYXQ+6EN5stWmeY/AZqtM8xk9k= -github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8/go.mod h1:K1liHPHnj73Fdn/EKuT8nrFqBihUSKXoLYU0BuatOYo= +github.com/google/pprof v0.0.0-20240903155634-a8630aee4ab9 h1:q5g0N9eal4bmJwXHC5z0QCKs8qhS35hFfq0BAYsIwZI= +github.com/google/pprof v0.0.0-20240903155634-a8630aee4ab9/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= @@ -32,15 +32,15 @@ github.com/mokiat/gog v0.13.1 h1:KHE6CsyRrfIpvfnAtjLT9P9u9eOHwBaTDuCDwWQgu98= github.com/mokiat/gog v0.13.1/go.mod h1:VZTey/yYMWG4vGWbrjIHwsjyodlw5UARL3DVwSHOSyw= github.com/mokiat/gomath v0.9.0 h1:dzhjuCouMIcw9BYOfWF70WbA8MlgyP0li9W1Be3HDfc= github.com/mokiat/gomath v0.9.0/go.mod h1:RiglRYxDOMcucDUBYLCN16wIsMfSHXtCuGkQ9gLcghE= -github.com/onsi/ginkgo/v2 v2.20.0 h1:PE84V2mHqoT1sglvHc8ZdQtPcwmvvt29WLEEO3xmdZw= -github.com/onsi/ginkgo/v2 v2.20.0/go.mod h1:lG9ey2Z29hR41WMVthyJBGUBcBhGOtoPF2VFMvBXFCI= -github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k= -github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY= +github.com/onsi/ginkgo/v2 v2.20.2 h1:7NVCeyIWROIAheY21RLS+3j2bb52W0W82tkberYytp4= +github.com/onsi/ginkgo/v2 v2.20.2/go.mod h1:K9gyxPIlb+aIvnZ8bd9Ak+YP18w3APlR+5coaZoE2ag= +github.com/onsi/gomega v1.34.2 h1:pNCwDkzrsv7MS9kpaQvVb1aVLahQXyJ/Tv5oAZMI3i8= +github.com/onsi/gomega v1.34.2/go.mod h1:v1xfxRgk0KIsG+QOdm7p8UosrOzPYRo60fd3B/1Dukc= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/qmuntal/gltf v0.26.0 h1:geVxZPBJ43f0ouLyseqMuzgKG6m6DkNH+gI3A63PFas= -github.com/qmuntal/gltf v0.26.0/go.mod h1:YoXZOt0Nc0kIfSKOLZIRoV4FycdC+GzE+3JgiAGYoMs= +github.com/qmuntal/gltf v0.27.0 h1:A6E5F9Efg/G8ke4OfzJtSDfaTKwXRx4OYCoXfslF534= +github.com/qmuntal/gltf v0.27.0/go.mod h1:YoXZOt0Nc0kIfSKOLZIRoV4FycdC+GzE+3JgiAGYoMs= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= @@ -48,18 +48,18 @@ github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcU github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= -golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa h1:ELnwvuAXPNtPk1TJRuGkI9fDTwym6AYBu0qzT8AcHdI= -golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ= -golang.org/x/image v0.19.0 h1:D9FX4QWkLfkeqaC62SonffIIuYdOk/UE2XKUBgRIBIQ= -golang.org/x/image v0.19.0/go.mod h1:y0zrRqlQRWQ5PXaYCOMLTW2fpsxZ8Qh9I/ohnInJEys= -golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= -golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= +golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e h1:I88y4caeGeuDQxgdoFPUq097j7kNfw6uvuiNxUBfcBk= +golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ= +golang.org/x/image v0.20.0 h1:7cVCUjQwfL18gyBJOmYvptfSHS8Fb3YUDtfLIZ7Nbpw= +golang.org/x/image v0.20.0/go.mod h1:0a88To4CYVBAHp5FXJm8o7QbUl37Vd85ply1vyD8auM= +golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo= +golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0= golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= -golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= -golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= +golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= +golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24= golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ= google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= diff --git a/render/api.go b/render/api.go index bc8ada65..4a8e21b5 100644 --- a/render/api.go +++ b/render/api.go @@ -35,6 +35,10 @@ type API interface { // used to store depth values. CreateDepthTexture2D(info DepthTexture2DInfo) Texture + // CreateDepthTexture2DArray creates a new 2D array Texture that can + // be used to store depth values. + CreateDepthTexture2DArray(info DepthTexture2DArrayInfo) Texture + // CreateStencilTexture2D creates a new 2D Texture that can be // used to store stencil values. CreateStencilTexture2D(info StencilTexture2DInfo) Texture diff --git a/render/command.go b/render/command.go index c89e7195..1cc9d1c6 100644 --- a/render/command.go +++ b/render/command.go @@ -22,6 +22,9 @@ type CommandBuffer interface { // specified framebuffer. BeginRenderPass(info RenderPassInfo) + // SetViewport changes the viewport settings of the render pass. + SetViewport(x, y, width, height uint32) + // BindPipeline configures the pipeline that should be used for the // following draw commands. BindPipeline(pipeline Pipeline) diff --git a/render/framebuffer.go b/render/framebuffer.go index eea97175..63f444e7 100644 --- a/render/framebuffer.go +++ b/render/framebuffer.go @@ -1,5 +1,7 @@ package render +import "github.com/mokiat/gog/opt" + // FramebufferMarker marks a type as being a Framebuffer. type FramebufferMarker interface { _isFramebufferType() @@ -20,19 +22,41 @@ type FramebufferInfo struct { // ColorAttachments is the list of color attachments that should be // attached to the Framebuffer. - ColorAttachments [4]Texture + ColorAttachments [4]opt.T[TextureAttachment] // DepthAttachment is the depth attachment that should be attached to // the Framebuffer. - DepthAttachment Texture + DepthAttachment opt.T[TextureAttachment] // StencilAttachment is the stencil attachment that should be attached // to the Framebuffer. - StencilAttachment Texture + StencilAttachment opt.T[TextureAttachment] // DepthStencilAttachment is the depth+stencil attachment that should // be attached to the Framebuffer. - DepthStencilAttachment Texture + DepthStencilAttachment opt.T[TextureAttachment] +} + +// PlainTextureAttachment creates a TextureAttachment that for the specified +// texture at the root mipmap layer and depth. +func PlainTextureAttachment(texture Texture) TextureAttachment { + return TextureAttachment{ + Texture: texture, + } +} + +// TextureAttachment represents a framebuffer attachment. +type TextureAttachment struct { + + // Texture is the texture that should be attached. + Texture Texture + + // Depth is the depth of the texture that should be attached, in case of a + // texture array or 3D texture. + Depth uint32 + + // MipmapLayer is the mipmap level of the texture that should be attached. + MipmapLayer uint32 } // CopyFramebufferToTextureInfo describes the configuration of a copy operation diff --git a/render/pass.go b/render/pass.go index f7331d3d..d0fa3b5c 100644 --- a/render/pass.go +++ b/render/pass.go @@ -26,6 +26,13 @@ type RenderPassInfo struct { // attachment when DepthLoadOp is LoadOperationClear. DepthClearValue float32 + // DepthBias configures a depth bias to be added to each fragment. + DepthBias float32 + + // DepthSlopeBias configures a slope-scaled depth bias to be added to each + // fragment. + DepthSlopeBias float32 + // StencilLoadOp describes how the contents of the stencil attachment should // be loaded. StencilLoadOp LoadOperation diff --git a/render/texture.go b/render/texture.go index a2c7deb6..b2c88666 100644 --- a/render/texture.go +++ b/render/texture.go @@ -10,6 +10,15 @@ type TextureMarker interface { type Texture interface { TextureMarker Resource + + // Width returns the width of the texture. + Width() uint32 + + // Height returns the height of the texture. + Height() uint32 + + // Depth returns the depth of the texture. + Depth() uint32 } const ( @@ -126,6 +135,23 @@ type DepthTexture2DInfo struct { Comparable bool } +// DepthTexture2DArrayInfo represents the information needed to create a +// 2D array depth Texture. +type DepthTexture2DArrayInfo struct { + + // Width specifies the width of the texture. + Width uint32 + + // Height specifies the height of the texture. + Height uint32 + + // Layers specifies the number of layers in the texture. + Layers uint32 + + // Comparable specifies whether the depth texture should be comparable. + Comparable bool +} + // StencilTexture2DInfo represents the information needed to create a // 2D stencil Texture. type StencilTexture2DInfo struct { diff --git a/ui/font_factory.go b/ui/font_factory.go index bc397f8d..6f501cd5 100644 --- a/ui/font_factory.go +++ b/ui/font_factory.go @@ -10,6 +10,7 @@ import ( "golang.org/x/image/font/sfnt" "golang.org/x/image/math/fixed" + "github.com/mokiat/gog/opt" "github.com/mokiat/gomath/sprec" "github.com/mokiat/lacking/render" ) @@ -80,10 +81,10 @@ func (f *fontFactory) Init() { }) f.framebuffer = f.api.CreateFramebuffer(render.FramebufferInfo{ - ColorAttachments: [4]render.Texture{ - f.colorTexture, + ColorAttachments: [4]opt.T[render.TextureAttachment]{ + opt.V(render.PlainTextureAttachment(f.colorTexture)), }, - StencilAttachment: f.stencilTexture, + StencilAttachment: opt.V(render.PlainTextureAttachment(f.stencilTexture)), }) } diff --git a/ui/std/viewport.go b/ui/std/viewport.go index d9edf3db..d51c1f64 100644 --- a/ui/std/viewport.go +++ b/ui/std/viewport.go @@ -161,8 +161,8 @@ func (c *viewportSurface) createFramebuffer(width, height uint32) { Format: render.DataFormatRGBA8, }) c.framebuffer = c.api.CreateFramebuffer(render.FramebufferInfo{ - ColorAttachments: [4]render.Texture{ - c.colorTexture, + ColorAttachments: [4]opt.T[render.TextureAttachment]{ + opt.V(render.PlainTextureAttachment(c.colorTexture)), }, }) } diff --git a/util/async/worker.go b/util/async/worker.go index 90d5d3bb..dcf42ace 100644 --- a/util/async/worker.go +++ b/util/async/worker.go @@ -26,11 +26,11 @@ func (w *Worker) Schedule(fn Func) { } func (w *Worker) ProcessCount(count int) bool { + w.pipelines.Add(1) + defer w.pipelines.Done() if atomic.LoadInt32(&w.running) == 0 { return false } - w.pipelines.Add(1) - defer w.pipelines.Done() for count > 0 { if !w.processNextTask() { @@ -42,11 +42,11 @@ func (w *Worker) ProcessCount(count int) bool { } func (w *Worker) ProcessDuration(targetDuration time.Duration) bool { + w.pipelines.Add(1) + defer w.pipelines.Done() if atomic.LoadInt32(&w.running) == 0 { return false } - w.pipelines.Add(1) - defer w.pipelines.Done() startTime := time.Now() for time.Since(startTime) < targetDuration { @@ -58,11 +58,11 @@ func (w *Worker) ProcessDuration(targetDuration time.Duration) bool { } func (w *Worker) ProcessAll() { + w.pipelines.Add(1) + defer w.pipelines.Done() if atomic.LoadInt32(&w.running) == 0 { return } - w.pipelines.Add(1) - defer w.pipelines.Done() for task := range w.tasks { task() @@ -72,10 +72,10 @@ func (w *Worker) ProcessAll() { func (w *Worker) Shutdown() { atomic.StoreInt32(&w.running, 0) close(w.tasks) + w.pipelines.Wait() for task := range w.tasks { task() } - w.pipelines.Wait() } func (w *Worker) processNextTask() bool { diff --git a/util/gltfutil/util.go b/util/gltfutil/util.go index 8af3117e..fb77f63f 100644 --- a/util/gltfutil/util.go +++ b/util/gltfutil/util.go @@ -11,17 +11,17 @@ import ( "github.com/qmuntal/gltf" ) -func RootNodeIndices(doc *gltf.Document) []uint32 { - childrenIDs := make(map[uint32]struct{}) +func RootNodeIndices(doc *gltf.Document) []int { + childrenIDs := make(map[int]struct{}) for _, node := range doc.Nodes { for _, childID := range node.Children { childrenIDs[childID] = struct{}{} } } - result := make([]uint32, 0, len(doc.Nodes)-len(childrenIDs)) + result := make([]int, 0, len(doc.Nodes)-len(childrenIDs)) for id := range doc.Nodes { - if _, ok := childrenIDs[uint32(id)]; !ok { - result = append(result, uint32(id)) + if _, ok := childrenIDs[id]; !ok { + result = append(result, id) } } return result @@ -285,7 +285,7 @@ func BaseColor(pbr *gltf.PBRMetallicRoughness) sprec.Vec4 { return sprec.NewVec4(float32(factor[0]), float32(factor[1]), float32(factor[2]), float32(factor[3])) } -func ColorTextureIndex(doc *gltf.Document, pbr *gltf.PBRMetallicRoughness) *uint32 { +func ColorTextureIndex(doc *gltf.Document, pbr *gltf.PBRMetallicRoughness) *int { colorTexture := pbr.BaseColorTexture if colorTexture == nil { return nil @@ -297,7 +297,7 @@ func ColorTextureIndex(doc *gltf.Document, pbr *gltf.PBRMetallicRoughness) *uint return &colorTexture.Index } -func MetallicRoughnessTextureIndex(doc *gltf.Document, pbr *gltf.PBRMetallicRoughness) *uint32 { +func MetallicRoughnessTextureIndex(doc *gltf.Document, pbr *gltf.PBRMetallicRoughness) *int { mrTexture := pbr.MetallicRoughnessTexture if mrTexture == nil { return nil @@ -309,7 +309,7 @@ func MetallicRoughnessTextureIndex(doc *gltf.Document, pbr *gltf.PBRMetallicRoug return &mrTexture.Index } -func NormalTextureIndexScale(doc *gltf.Document, material *gltf.Material) (*uint32, float32) { +func NormalTextureIndexScale(doc *gltf.Document, material *gltf.Material) (*int, float32) { normalTexture := material.NormalTexture if normalTexture == nil { return nil, 1.0 @@ -474,7 +474,7 @@ func AnimationScales(doc *gltf.Document, sampler *gltf.AnimationSampler) []dprec } } -func BufferViewData(doc *gltf.Document, index uint32) gblob.LittleEndianBlock { +func BufferViewData(doc *gltf.Document, index int) gblob.LittleEndianBlock { bufferView := doc.BufferViews[index] offset := bufferView.ByteOffset count := bufferView.ByteLength