From 0542baab308343356c61e2456b0c8c77a5a599c5 Mon Sep 17 00:00:00 2001 From: Colt Daily Date: Fri, 5 Jul 2024 12:10:31 -0400 Subject: [PATCH] scene-graph: rework rendering & canvas coordinates calculations for CanvasLayer nodes (#254) * scene-graph: rework rendering with CanvasLayers * scene-graph: fix input screen coordinates calculations from CanvasLayerContainer to CanvasLayer * scene-graph: add back optional batch param in constructor --- .../com/littlekt/util/viewport/ViewportExt.kt | 13 ----- .../com/littlekt/examples/FontExample.kt | 2 - .../examples/GameWorldAndUIViewports.kt | 11 ++-- .../examples/HelloSceneGraphExample.kt | 4 +- .../examples/LDtkTileMapCacheExample.kt | 2 - .../littlekt/examples/LDtkTileMapExample.kt | 2 - .../littlekt/examples/SimpleCameraExample.kt | 2 - .../examples/TiledTileMapCacheExample.kt | 2 - .../littlekt/examples/TiledTileMapExample.kt | 2 - .../kotlin/com/littlekt/graph/SceneGraph.kt | 51 ++++++++----------- .../com/littlekt/graph/node/CanvasLayer.kt | 26 ++++------ .../graph/node/ui/CanvasLayerContainer.kt | 15 ++++++ 12 files changed, 55 insertions(+), 77 deletions(-) delete mode 100644 core/src/commonMain/kotlin/com/littlekt/util/viewport/ViewportExt.kt diff --git a/core/src/commonMain/kotlin/com/littlekt/util/viewport/ViewportExt.kt b/core/src/commonMain/kotlin/com/littlekt/util/viewport/ViewportExt.kt deleted file mode 100644 index d3e2d3423..000000000 --- a/core/src/commonMain/kotlin/com/littlekt/util/viewport/ViewportExt.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.littlekt.util.viewport - -import com.littlekt.graphics.webgpu.RenderPassEncoder - -/** - * Sets the viewport used during the rasterization stage to linear map from normalized device - * coordinates to viewport coordinates. - * - * @param viewport viewport data use on the render pass - */ -fun RenderPassEncoder.setViewport(viewport: Viewport) { - setViewport(viewport.x, viewport.y, viewport.width, viewport.height) -} diff --git a/examples/src/commonMain/kotlin/com/littlekt/examples/FontExample.kt b/examples/src/commonMain/kotlin/com/littlekt/examples/FontExample.kt index 5751e939d..85a83b846 100644 --- a/examples/src/commonMain/kotlin/com/littlekt/examples/FontExample.kt +++ b/examples/src/commonMain/kotlin/com/littlekt/examples/FontExample.kt @@ -10,7 +10,6 @@ import com.littlekt.graphics.g2d.use import com.littlekt.graphics.webgpu.* import com.littlekt.resources.Fonts import com.littlekt.util.viewport.ExtendViewport -import com.littlekt.util.viewport.setViewport /** * @author Colton Daily @@ -89,7 +88,6 @@ class FontExample(context: Context) : ContextListener(context) { ) ) ) - renderPassEncoder.setViewport(viewport) camera.update() batch.use(renderPassEncoder, camera.viewProjection) { diff --git a/examples/src/commonMain/kotlin/com/littlekt/examples/GameWorldAndUIViewports.kt b/examples/src/commonMain/kotlin/com/littlekt/examples/GameWorldAndUIViewports.kt index 223418dae..3047fde43 100644 --- a/examples/src/commonMain/kotlin/com/littlekt/examples/GameWorldAndUIViewports.kt +++ b/examples/src/commonMain/kotlin/com/littlekt/examples/GameWorldAndUIViewports.kt @@ -7,7 +7,6 @@ import com.littlekt.graph.node.ui.centerContainer import com.littlekt.graph.node.ui.label import com.littlekt.graph.sceneGraph import com.littlekt.graphics.g2d.SpriteBatch -import com.littlekt.graphics.g2d.use import com.littlekt.graphics.webgpu.* import com.littlekt.util.viewport.ExtendViewport @@ -46,7 +45,7 @@ class GameWorldAndUIViewports(context: Context) : ContextListener(context) { if (preferredFormat.srgb) world.defaultLevelBackgroundColor.toLinear() else world.defaultLevelBackgroundColor val graph = - sceneGraph(this, viewport = ExtendViewport(960, 540)) { + sceneGraph(this, viewport = ExtendViewport(960, 540), batch = batch) { centerContainer { anchorRight = 1f anchorTop = 1f @@ -107,11 +106,10 @@ class GameWorldAndUIViewports(context: Context) : ContextListener(context) { ) ) ) - // worldRenderPass.setViewport(worldViewport) worldCamera.update() - batch.use(worldRenderPass, worldCamera.viewProjection) { - world.render(batch, worldCamera, scale = 1f) - } + batch.begin(worldCamera.viewProjection) + world.render(batch, worldCamera, scale = 1f) + batch.flush(worldRenderPass) worldRenderPass.end() worldRenderPass.release() @@ -129,6 +127,7 @@ class GameWorldAndUIViewports(context: Context) : ContextListener(context) { graph.update(dt) graph.render(commandEncoder, uiRenderPassDescriptor) + batch.end() val commandBuffer = commandEncoder.finish() diff --git a/examples/src/commonMain/kotlin/com/littlekt/examples/HelloSceneGraphExample.kt b/examples/src/commonMain/kotlin/com/littlekt/examples/HelloSceneGraphExample.kt index cc1316501..a3a39e1f6 100644 --- a/examples/src/commonMain/kotlin/com/littlekt/examples/HelloSceneGraphExample.kt +++ b/examples/src/commonMain/kotlin/com/littlekt/examples/HelloSceneGraphExample.kt @@ -16,6 +16,7 @@ import com.littlekt.input.Key import com.littlekt.math.geom.Angle import com.littlekt.math.geom.degrees import com.littlekt.math.geom.radians +import com.littlekt.util.viewport.ExtendViewport /** * An example using a [sceneGraph] @@ -42,7 +43,7 @@ class HelloSceneGraphExample(context: Context) : ContextListener(context) { ) val graph = - sceneGraph(this) { + sceneGraph(this, ExtendViewport(960, 540)) { canvasLayerContainer { stretch = true shrink = 2 @@ -59,7 +60,6 @@ class HelloSceneGraphExample(context: Context) : ContextListener(context) { } } } - // button { text = "test" } node2d { rotation = 45.degrees onReady += { println("$name: $canvas") } diff --git a/examples/src/commonMain/kotlin/com/littlekt/examples/LDtkTileMapCacheExample.kt b/examples/src/commonMain/kotlin/com/littlekt/examples/LDtkTileMapCacheExample.kt index 817b36076..6e1ea3e47 100644 --- a/examples/src/commonMain/kotlin/com/littlekt/examples/LDtkTileMapCacheExample.kt +++ b/examples/src/commonMain/kotlin/com/littlekt/examples/LDtkTileMapCacheExample.kt @@ -6,7 +6,6 @@ import com.littlekt.file.vfs.readLDtkMapLoader import com.littlekt.graphics.g2d.SpriteCache import com.littlekt.graphics.webgpu.* import com.littlekt.util.viewport.ExtendViewport -import com.littlekt.util.viewport.setViewport /** * Load and render an entire world of LDtk. @@ -97,7 +96,6 @@ class LDtkTileMapCacheExample(context: Context) : ContextListener(context) { ) ) - renderPassEncoder.setViewport(viewport) camera.update() cache.render(renderPassEncoder, camera.viewProjection) diff --git a/examples/src/commonMain/kotlin/com/littlekt/examples/LDtkTileMapExample.kt b/examples/src/commonMain/kotlin/com/littlekt/examples/LDtkTileMapExample.kt index 8953e19d3..e7802bf01 100644 --- a/examples/src/commonMain/kotlin/com/littlekt/examples/LDtkTileMapExample.kt +++ b/examples/src/commonMain/kotlin/com/littlekt/examples/LDtkTileMapExample.kt @@ -7,7 +7,6 @@ import com.littlekt.graphics.g2d.SpriteBatch import com.littlekt.graphics.g2d.use import com.littlekt.graphics.webgpu.* import com.littlekt.util.viewport.ExtendViewport -import com.littlekt.util.viewport.setViewport /** * Load and render an LDtk map. @@ -93,7 +92,6 @@ class LDtkTileMapExample(context: Context) : ContextListener(context) { ) ) ) - renderPassEncoder.setViewport(viewport) camera.update() batch.use(renderPassEncoder, camera.viewProjection) { diff --git a/examples/src/commonMain/kotlin/com/littlekt/examples/SimpleCameraExample.kt b/examples/src/commonMain/kotlin/com/littlekt/examples/SimpleCameraExample.kt index 568365041..166952430 100644 --- a/examples/src/commonMain/kotlin/com/littlekt/examples/SimpleCameraExample.kt +++ b/examples/src/commonMain/kotlin/com/littlekt/examples/SimpleCameraExample.kt @@ -7,7 +7,6 @@ import com.littlekt.graphics.Color import com.littlekt.graphics.g2d.SpriteBatch import com.littlekt.graphics.webgpu.* import com.littlekt.util.viewport.ExtendViewport -import com.littlekt.util.viewport.setViewport /** * An example using a simple Orthographic camera to move around a texture. @@ -88,7 +87,6 @@ class SimpleCameraExample(context: Context) : ContextListener(context) { ) ) ) - renderPassEncoder.setViewport(viewport) camera.update() batch.begin() batch.draw(logoTexture, 0f, 0f) diff --git a/examples/src/commonMain/kotlin/com/littlekt/examples/TiledTileMapCacheExample.kt b/examples/src/commonMain/kotlin/com/littlekt/examples/TiledTileMapCacheExample.kt index d4fbf939f..0789b86bd 100644 --- a/examples/src/commonMain/kotlin/com/littlekt/examples/TiledTileMapCacheExample.kt +++ b/examples/src/commonMain/kotlin/com/littlekt/examples/TiledTileMapCacheExample.kt @@ -7,7 +7,6 @@ import com.littlekt.graphics.Color import com.littlekt.graphics.g2d.SpriteCache import com.littlekt.graphics.webgpu.* import com.littlekt.util.viewport.ExtendViewport -import com.littlekt.util.viewport.setViewport /** * Load and render a Tiled map using a [SpriteCache]. @@ -91,7 +90,6 @@ class TiledTileMapCacheExample(context: Context) : ContextListener(context) { ) ) ) - renderPassEncoder.setViewport(viewport) camera.update() map.updateCachedAnimationTiles(cache) cache.render(renderPassEncoder, camera.viewProjection) diff --git a/examples/src/commonMain/kotlin/com/littlekt/examples/TiledTileMapExample.kt b/examples/src/commonMain/kotlin/com/littlekt/examples/TiledTileMapExample.kt index e7f0793c0..1020dfce5 100644 --- a/examples/src/commonMain/kotlin/com/littlekt/examples/TiledTileMapExample.kt +++ b/examples/src/commonMain/kotlin/com/littlekt/examples/TiledTileMapExample.kt @@ -9,7 +9,6 @@ import com.littlekt.graphics.g2d.shape.ShapeRenderer import com.littlekt.graphics.g2d.use import com.littlekt.graphics.webgpu.* import com.littlekt.util.viewport.ExtendViewport -import com.littlekt.util.viewport.setViewport /** * Load and render a Tiled map. @@ -93,7 +92,6 @@ class TiledTileMapExample(context: Context) : ContextListener(context) { ) ) ) - renderPassEncoder.setViewport(viewport) camera.update() batch.use(renderPassEncoder, camera.viewProjection) { diff --git a/scene-graph/src/commonMain/kotlin/com/littlekt/graph/SceneGraph.kt b/scene-graph/src/commonMain/kotlin/com/littlekt/graph/SceneGraph.kt index 5403a3e12..e220bc755 100644 --- a/scene-graph/src/commonMain/kotlin/com/littlekt/graph/SceneGraph.kt +++ b/scene-graph/src/commonMain/kotlin/com/littlekt/graph/SceneGraph.kt @@ -33,6 +33,7 @@ import kotlin.time.Duration.Companion.milliseconds * * @param context the current context * @param viewport the viewport that the camera of the scene graph will own + * @param batch an option sprite batch. If omitted, the scene graph will create and manage its own. * @param callback the callback that is invoked with a [SceneGraph] context in order to initialize * any values and create nodes * @return the newly created [SceneGraph] @@ -41,6 +42,7 @@ import kotlin.time.Duration.Companion.milliseconds inline fun sceneGraph( context: Context, viewport: Viewport = ScreenViewport(context.graphics.width, context.graphics.height), + batch: Batch? = null, controller: InputMapController? = null, whitePixel: TextureSlice = Textures.white, callback: @SceneGraphDslMarker SceneGraph.() -> Unit = {}, @@ -63,6 +65,7 @@ inline fun sceneGraph( return SceneGraph( context, viewport, + batch, signals, controller ?: createDefaultSceneGraphController(context.input, signals), whitePixel @@ -75,6 +78,7 @@ inline fun sceneGraph( * * @param context the current context * @param viewport the viewport that the camera of the scene graph will own + * @param batch an option sprite batch. If omitted, the scene graph will create and manage its own. * @param callback the callback that is invoked with a [SceneGraph] context in order to initialize * any values and create nodes * @return the newly created [SceneGraph] @@ -83,13 +87,15 @@ inline fun sceneGraph( inline fun sceneGraph( context: Context, viewport: Viewport = ScreenViewport(context.graphics.width, context.graphics.height), + batch: Batch? = null, uiInputSignals: SceneGraph.UiInputSignals = SceneGraph.UiInputSignals(), controller: InputMapController = InputMapController(context.input), whitePixel: TextureSlice = Textures.white, callback: @SceneGraphDslMarker SceneGraph.() -> Unit = {}, ): SceneGraph { contract { callsInPlace(callback, InvocationKind.EXACTLY_ONCE) } - return SceneGraph(context, viewport, uiInputSignals, controller, whitePixel).also(callback) + return SceneGraph(context, viewport, batch, uiInputSignals, controller, whitePixel) + .also(callback) } /** @@ -146,11 +152,11 @@ fun createDefaultSceneGraphController( InputMapController(input).also { it.addDefaultUiInput(uiInputSignals) } /** - * A class for creating a scene graph of nodes. The scene graph manages its own [Batch] and handles - * creating, ending, and releasing [RenderPassEncoder]. + * A class for creating a scene graph of nodes. * * @param context the current context * @param viewport the viewport that the camera of the scene graph will own + * @param batch an option sprite batch. If omitted, the scene graph will create and manage its own. * @param uiInputSignals the input signals mapped to the UI input of type [InputType]. * @param controller the input map controller for the scene graph * @param whitePixel a white 1x1 pixel [TextureSlice] that is used for rendering with @@ -161,18 +167,25 @@ fun createDefaultSceneGraphController( open class SceneGraph( val context: Context, viewport: Viewport = ScreenViewport(context.graphics.width, context.graphics.height), + batch: Batch? = null, val uiInputSignals: UiInputSignals = UiInputSignals(), val controller: InputMapController = createDefaultSceneGraphController(context.input, uiInputSignals), whitePixel: TextureSlice = Textures.white, ) : InputMapProcessor, Releasable { + private var ownsBatch = true /** * The [Batch] being used by the scene graph. This batch may be managed by the scene graph or * could be optional passed in via the constructor. */ val batch: Batch = - SpriteBatch(context.graphics.device, context.graphics, context.graphics.preferredFormat) + batch?.also { ownsBatch = false } + ?: SpriteBatch( + context.graphics.device, + context.graphics, + context.graphics.preferredFormat + ) /** The [ShapeRenderer] managed by the scene graph. This uses the [batch] for drawing. */ val shapeRenderer: ShapeRenderer = ShapeRenderer(this.batch, whitePixel) @@ -185,7 +198,6 @@ open class SceneGraph( CanvasLayer().apply { name = "Scene Viewport" this.viewport = viewport - spriteShader = this@SceneGraph.batch.defaultShader resizeAutomatically = false } } @@ -337,29 +349,8 @@ open class SceneGraph( } this.commandEncoder = commandEncoder - if (dirty) { - sceneCanvas.resizeFbo(width.toInt(), height.toInt()) - dirty = false - } + sceneCanvas.canvasRenderPassDescriptor = renderPassDescriptor sceneCanvas.render(batch, shapeRenderer) { node, _, _, _, _ -> checkNodeMaterial(node) } - - if (batch.drawing) { - batch.setBlendState(BlendState.NonPreMultiplied) - batch.useDefaultShader() - val graphPass = commandEncoder.beginRenderPass(desc = renderPassDescriptor) - batch.viewProjection = sceneCanvas.canvasCamera.viewProjection - batch.draw( - texture = sceneCanvas.target, - x = 0f, - y = 0f, - width = sceneCanvas.width.toFloat(), - height = sceneCanvas.height.toFloat() - ) - batch.flush(graphPass) - graphPass.end() - graphPass.release() - } - flush() if (debugInfoDirty) { @@ -384,7 +375,7 @@ open class SceneGraph( commandEncoder = null - if (batch.drawing) { + if (ownsBatch && batch.drawing) { batch.end() } } @@ -1092,7 +1083,9 @@ open class SceneGraph( */ override fun release() { sceneCanvas.destroy() - batch.release() + if (ownsBatch) { + batch.release() + } controller.removeInputMapProcessor(this) context.input.removeInputProcessor(this) diff --git a/scene-graph/src/commonMain/kotlin/com/littlekt/graph/node/CanvasLayer.kt b/scene-graph/src/commonMain/kotlin/com/littlekt/graph/node/CanvasLayer.kt index cb5d08c77..1b58767cd 100644 --- a/scene-graph/src/commonMain/kotlin/com/littlekt/graph/node/CanvasLayer.kt +++ b/scene-graph/src/commonMain/kotlin/com/littlekt/graph/node/CanvasLayer.kt @@ -17,6 +17,7 @@ import com.littlekt.log.Logger import com.littlekt.math.Mat4 import com.littlekt.math.MutableVec2f import com.littlekt.math.MutableVec3f +import com.littlekt.util.viewport.ScreenViewport import com.littlekt.util.viewport.Viewport import kotlin.contracts.ExperimentalContracts import kotlin.contracts.InvocationKind @@ -72,7 +73,7 @@ open class CanvasLayer : Node() { * * @see CanvasLayerContainer */ - var viewport: Viewport = Viewport() + var viewport: Viewport = ScreenViewport(0, 0) /** The [OrthographicCamera] of this [CanvasLayer]. This may be manipulated. */ val canvasCamera: OrthographicCamera @@ -236,9 +237,6 @@ open class CanvasLayer : Node() { viewport.update(width, height, true) canvasCamera3d.virtualWidth = width.toFloat() canvasCamera3d.virtualHeight = height.toFloat() - canvasCamera.ortho(width, height) - viewport.width = width - viewport.height = height onSizeChanged.emit() super.resize(width, height) @@ -290,11 +288,11 @@ open class CanvasLayer : Node() { if (canvasRenderPass != null && batch.drawing) { batch.flush(canvasRenderPass) } + canvas?.let { popAndEndCanvasRenderPass() } batch.shader = spriteShader canvasCamera.update() canvasCamera3d.update() batch.viewProjection = canvasCamera.viewProjection - canvas?.let { popAndEndCanvasRenderPass() } pushRenderPass(renderPassDescriptor.label, renderPassDescriptor) } @@ -353,8 +351,8 @@ open class CanvasLayer : Node() { scene ?: return null if (!enabled || isDestroyed) return null tempVec.set( - hx - width * 0.5f + canvasCamera.position.x, - hy - height * 0.5f + canvasCamera.position.y + hx - virtualWidth * 0.5f + canvasCamera.position.x, + hy - virtualHeight * 0.5f + canvasCamera.position.y ) // we don't need to convert to canvas coords because the FrameBufferContainer handles // all of that. We just need to pass it down @@ -371,8 +369,8 @@ open class CanvasLayer : Node() { scene ?: return false if (!enabled || isDestroyed) return false tempVec.set( - event.canvasX - width * 0.5f + canvasCamera.position.x, - event.canvasY - height * 0.5f + canvasCamera.position.y + event.canvasX - virtualWidth * 0.5f + canvasCamera.position.x, + event.canvasY - virtualHeight * 0.5f + canvasCamera.position.y ) nodes.forEachReversed { // we set canvas coords every iteration just in case a child CanvasLayer changes it @@ -391,8 +389,8 @@ open class CanvasLayer : Node() { scene ?: return false if (!enabled || isDestroyed) return false tempVec.set( - event.canvasX - width * 0.5f + canvasCamera.position.x, - event.canvasY - height * 0.5f + canvasCamera.position.y + event.canvasX - virtualWidth * 0.5f + canvasCamera.position.x, + event.canvasY - virtualHeight * 0.5f + canvasCamera.position.y ) nodes.forEachReversed { // we set canvas coords every iteration just in case a child CanvasLayer changes it @@ -412,13 +410,12 @@ open class CanvasLayer : Node() { if (!enabled || isDestroyed) return if (width == 0 || height == 0) return if (batch.drawing) { - canvasCamera.update() - batch.flush(renderPass, canvasCamera.viewProjection) + batch.flush(renderPass) } + popAndEndRenderPass() batch.viewProjection = prevProjection batch.shader = prevShader ?: error("Unable to set Batch.shader back to its previous shader!") - popAndEndRenderPass() canvas?.let { pushRenderPassToCanvas("${canvas?.name} pass") } if (renderPasses.isNotEmpty()) { logger.warn { @@ -490,7 +487,6 @@ open class CanvasLayer : Node() { ?: error("Command encoder has not been set on the graph!") renderPasses += result renderPassOrNull = result - // result.setViewport(viewport) } /** diff --git a/scene-graph/src/commonMain/kotlin/com/littlekt/graph/node/ui/CanvasLayerContainer.kt b/scene-graph/src/commonMain/kotlin/com/littlekt/graph/node/ui/CanvasLayerContainer.kt index 40d52a483..11a2e16f2 100644 --- a/scene-graph/src/commonMain/kotlin/com/littlekt/graph/node/ui/CanvasLayerContainer.kt +++ b/scene-graph/src/commonMain/kotlin/com/littlekt/graph/node/ui/CanvasLayerContainer.kt @@ -172,9 +172,14 @@ open class CanvasLayerContainer : Container() { if (stretch) { temp.scale(1f / shrink.toFloat()) } + temp.scale(1f / globalScaleX, 1f / globalScaleY) nodes.forEachReversed { val target = if (it is CanvasLayer) { + temp.scale( + 1f / (width / it.virtualWidth) * shrink, + 1f / (height / it.virtualHeight) * shrink + ) it.propagateHit(temp.x, temp.y) } else { it.propagateHit(hx, hy) @@ -194,11 +199,16 @@ open class CanvasLayerContainer : Container() { if (stretch) { temp.scale(1f / shrink.toFloat()) } + temp.scale(1f / globalScaleX, 1f / globalScaleY) val prevCanvasX = event.canvasX val prevCanvasY = event.canvasY nodes.forEachReversed { // we set canvas coords every iteration just in case a child CanvasLayer changes it if (it is CanvasLayer) { + temp.scale( + 1f / (width / it.virtualWidth) * shrink, + 1f / (height / it.virtualHeight) * shrink + ) event.canvasX = temp.x event.canvasY = temp.y } else { @@ -221,11 +231,16 @@ open class CanvasLayerContainer : Container() { if (stretch) { temp.scale(1f / shrink.toFloat()) } + temp.scale(1f / globalScaleX, 1f / globalScaleY) val prevCanvasX = event.canvasX val prevCanvasY = event.canvasY nodes.forEachReversed { // we set canvas coords every iteration just in case a child CanvasLayer changes it if (it is CanvasLayer) { + temp.scale( + 1f / (width / it.virtualWidth) * shrink, + 1f / (height / it.virtualHeight) * shrink + ) event.canvasX = temp.x event.canvasY = temp.y } else {