From 5a745db7c69265c35ea3bb2c626b29af73dc961b Mon Sep 17 00:00:00 2001 From: Emanuel Rabina Date: Sat, 27 Jan 2024 14:12:20 +1300 Subject: [PATCH] =?UTF-8?q?Start=20on=20scripting=20and=20off-thread=20req?= =?UTF-8?q?uests=20of=20the=20graphics=20system=20=F0=9F=A5=B3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ultraq/redhorizon/cli/FileOptions.groovy | 19 ++-- .../cli/mediaplayer/ImageScript.groovy | 41 +++++++++ .../cli/mediaplayer/MediaPlayer.groovy | 55 +++++------- .../cli/mediaplayer/MediaPlayerCli.groovy | 12 +-- .../engine/graphics/GraphicsRequests.groovy | 59 +++++++++++++ .../engine/graphics/GraphicsSystem.groovy | 79 ++++++++++++++++- .../graphics/pipeline/RenderPipeline.groovy | 2 - .../redhorizon/engine/media/Image.groovy | 3 +- .../engine/media/ImageLoader.groovy | 22 +---- .../engine/media/MediaLoader.groovy | 6 +- .../redhorizon/engine/scenegraph/Node.groovy | 18 ++-- .../redhorizon/engine/scenegraph/Scene.groovy | 24 ++++- ...Positionable.groovy => SceneEvents.groovy} | 24 ++--- .../engine/scenegraph/nodes/Sprite.groovy | 88 +++++++++++++++++++ .../SpriteScript.groovy} | 31 +++---- .../redhorizon/engine/scripting/Script.groovy | 44 ++++++++++ .../engine/scripting/Scriptable.groovy | 40 +++++++++ .../redhorizon/explorer/Explorer.groovy | 4 +- 18 files changed, 462 insertions(+), 109 deletions(-) create mode 100644 redhorizon-cli/source/nz/net/ultraq/redhorizon/cli/mediaplayer/ImageScript.groovy create mode 100644 redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/graphics/GraphicsRequests.groovy rename redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/scenegraph/{Positionable.groovy => SceneEvents.groovy} (68%) create mode 100644 redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/scenegraph/nodes/Sprite.groovy rename redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/scenegraph/{Selectable.groovy => nodes/SpriteScript.groovy} (55%) create mode 100644 redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/scripting/Script.groovy create mode 100644 redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/scripting/Scriptable.groovy diff --git a/redhorizon-cli/source/nz/net/ultraq/redhorizon/cli/FileOptions.groovy b/redhorizon-cli/source/nz/net/ultraq/redhorizon/cli/FileOptions.groovy index 28e83387..0ce11457 100644 --- a/redhorizon-cli/source/nz/net/ultraq/redhorizon/cli/FileOptions.groovy +++ b/redhorizon-cli/source/nz/net/ultraq/redhorizon/cli/FileOptions.groovy @@ -1,12 +1,12 @@ -/* +/* * Copyright 2021, Emanuel Rabina (http://www.ultraq.net.nz/) - * + * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -21,9 +21,12 @@ import nz.net.ultraq.redhorizon.classic.filetypes.MixFile import org.slf4j.Logger import picocli.CommandLine.Parameters +import groovy.transform.stc.ClosureParams +import groovy.transform.stc.SimpleType + /** * A mixin of shareable file options. - * + * * @author Emanuel Rabina */ class FileOptions { @@ -37,11 +40,9 @@ class FileOptions { /** * Load the file indicated by the {@code file} and {@code entryName} * parameters, passing it along to the given closure. - * - * @param logger - * @param closure */ - void useFile(Logger logger, Closure closure) { + void useFile(Logger logger, + @ClosureParams(value = SimpleType, options = 'nz.net.ultraq.redhorizon.filetypes.ResourceFile') Closure closure) { logger.info('Loading {}...', file) diff --git a/redhorizon-cli/source/nz/net/ultraq/redhorizon/cli/mediaplayer/ImageScript.groovy b/redhorizon-cli/source/nz/net/ultraq/redhorizon/cli/mediaplayer/ImageScript.groovy new file mode 100644 index 00000000..4c820c0c --- /dev/null +++ b/redhorizon-cli/source/nz/net/ultraq/redhorizon/cli/mediaplayer/ImageScript.groovy @@ -0,0 +1,41 @@ +/* + * Copyright 2024, Emanuel Rabina (http://www.ultraq.net.nz/) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package nz.net.ultraq.redhorizon.cli.mediaplayer + +import nz.net.ultraq.redhorizon.engine.scenegraph.Scene +import nz.net.ultraq.redhorizon.engine.scenegraph.nodes.SpriteScript + +import groovy.transform.InheritConstructors + +/** + * A script to make a sprite node behave as a full-screen image. + * + * @author Emanuel Rabina + */ +@InheritConstructors +class ImageScript extends SpriteScript { + + @Override + void onSceneAdded(Scene scene) { + + var width = sprite.imageFile.width + var height = sprite.imageFile.height + sprite + .scaleXY(scene.window.renderResolution.calculateScaleToFit(width, height)) + .translate(-width / 2, -height / 2) + } +} diff --git a/redhorizon-cli/source/nz/net/ultraq/redhorizon/cli/mediaplayer/MediaPlayer.groovy b/redhorizon-cli/source/nz/net/ultraq/redhorizon/cli/mediaplayer/MediaPlayer.groovy index a4b20d15..ca4f95f1 100644 --- a/redhorizon-cli/source/nz/net/ultraq/redhorizon/cli/mediaplayer/MediaPlayer.groovy +++ b/redhorizon-cli/source/nz/net/ultraq/redhorizon/cli/mediaplayer/MediaPlayer.groovy @@ -20,21 +20,11 @@ import nz.net.ultraq.redhorizon.engine.Application import nz.net.ultraq.redhorizon.engine.audio.AudioConfiguration import nz.net.ultraq.redhorizon.engine.graphics.GraphicsConfiguration import nz.net.ultraq.redhorizon.engine.input.KeyEvent -import nz.net.ultraq.redhorizon.engine.media.AnimationLoader -import nz.net.ultraq.redhorizon.engine.media.ImageLoader -import nz.net.ultraq.redhorizon.engine.media.ImagesLoader import nz.net.ultraq.redhorizon.engine.media.MediaLoader -import nz.net.ultraq.redhorizon.engine.media.Playable -import nz.net.ultraq.redhorizon.engine.media.SoundLoader -import nz.net.ultraq.redhorizon.engine.media.StopEvent -import nz.net.ultraq.redhorizon.engine.media.VideoLoader -import nz.net.ultraq.redhorizon.filetypes.AnimationFile +import nz.net.ultraq.redhorizon.engine.scenegraph.nodes.Sprite import nz.net.ultraq.redhorizon.filetypes.ImageFile -import nz.net.ultraq.redhorizon.filetypes.ImagesFile import nz.net.ultraq.redhorizon.filetypes.Palette import nz.net.ultraq.redhorizon.filetypes.ResourceFile -import nz.net.ultraq.redhorizon.filetypes.SoundFile -import nz.net.ultraq.redhorizon.filetypes.VideoFile import org.slf4j.Logger import org.slf4j.LoggerFactory @@ -64,16 +54,11 @@ class MediaPlayer extends Application { /** * Constructor, create a new application around the given media file. - * - * @param mediaFile - * @param audioConfig - * @param graphicsConfig - * @param paletteType */ - MediaPlayer(ResourceFile mediaFile, AudioConfiguration audioConfig, GraphicsConfiguration graphicsConfig, Palette palette) { + MediaPlayer(ResourceFile resourceFile, AudioConfiguration audioConfig, GraphicsConfiguration graphicsConfig, Palette palette) { super('Media Player', audioConfig, graphicsConfig) - this.mediaFile = mediaFile + this.mediaFile = resourceFile this.palette = palette } @@ -82,22 +67,28 @@ class MediaPlayer extends Application { logger.info('File details: {}', mediaFile) - mediaLoader = switch (mediaFile) { - case VideoFile -> new VideoLoader(mediaFile, scene, graphicsSystem, gameClock, inputEventStream) - case AnimationFile -> new AnimationLoader(mediaFile, scene, graphicsSystem, gameClock, inputEventStream) - case SoundFile -> new SoundLoader(mediaFile, scene, gameClock, inputEventStream) - case ImageFile -> new ImageLoader(mediaFile, scene, graphicsSystem) - case ImagesFile -> new ImagesLoader(mediaFile, palette, scene, graphicsSystem, inputEventStream) - default -> throw new UnsupportedOperationException("No media player for the associated file class of ${mediaFile}") + switch (mediaFile) { + case ImageFile: + scene << new Sprite(mediaFile).attachScript(new ImageScript()) + break } - var media = mediaLoader.load() - if (media instanceof Playable) { - media.on(StopEvent) { event -> - stop() - } - media.play() - } +// mediaLoader = switch (mediaFile) { +// case VideoFile -> new VideoLoader(mediaFile, scene, graphicsSystem, gameClock, inputEventStream) +// case AnimationFile -> new AnimationLoader(mediaFile, scene, graphicsSystem, gameClock, inputEventStream) +// case SoundFile -> new SoundLoader(mediaFile, scene, gameClock, inputEventStream) +// case ImageFile -> new ImageLoader(mediaFile, scene) +// case ImagesFile -> new ImagesLoader(mediaFile, palette, scene, graphicsSystem, inputEventStream) +// default -> throw new UnsupportedOperationException("No media player for the associated file class of ${mediaFile}") +// } +// +// var media = mediaLoader.load() +// if (media instanceof Playable) { +// media.on(StopEvent) { event -> +// stop() +// } +// media.play() +// } // Universal quit on exit inputEventStream.on(KeyEvent) { event -> diff --git a/redhorizon-cli/source/nz/net/ultraq/redhorizon/cli/mediaplayer/MediaPlayerCli.groovy b/redhorizon-cli/source/nz/net/ultraq/redhorizon/cli/mediaplayer/MediaPlayerCli.groovy index 41767f62..e2e9eca5 100644 --- a/redhorizon-cli/source/nz/net/ultraq/redhorizon/cli/mediaplayer/MediaPlayerCli.groovy +++ b/redhorizon-cli/source/nz/net/ultraq/redhorizon/cli/mediaplayer/MediaPlayerCli.groovy @@ -1,12 +1,12 @@ -/* +/* * Copyright 2016, Emanuel Rabina (http://www.ultraq.net.nz/) - * + * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -84,8 +84,8 @@ class MediaPlayerCli implements Callable { renderResolution: new Dimension(1280, 800) ) - fileOptions.useFile(logger) { mediaFile -> - new MediaPlayer(mediaFile, audioConfig, graphicsConfig, paletteOptions.loadPalette()).start() + fileOptions.useFile(logger) { resourceFile -> + new MediaPlayer(resourceFile, audioConfig, graphicsConfig, paletteOptions.loadPalette()).start() } return 0 diff --git a/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/graphics/GraphicsRequests.groovy b/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/graphics/GraphicsRequests.groovy new file mode 100644 index 00000000..c7b31d04 --- /dev/null +++ b/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/graphics/GraphicsRequests.groovy @@ -0,0 +1,59 @@ +/* + * Copyright 2024, Emanuel Rabina (http://www.ultraq.net.nz/) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package nz.net.ultraq.redhorizon.engine.graphics + +import nz.net.ultraq.redhorizon.filetypes.ColourFormat + +import org.joml.primitives.Rectanglef + +import groovy.transform.ImmutableOptions +import java.nio.ByteBuffer +import java.util.concurrent.Future + +/** + * Interface to make requests of the graphics system. + * + * @author Emanuel Rabina + */ +interface GraphicsRequests { + + static interface Request {} + + static record ShaderRequest(String name) implements Request {} + + @ImmutableOptions(knownImmutables = ['surface']) + static record SpriteMeshRequest(Rectanglef surface) implements Request {} + + @ImmutableOptions(knownImmutables = ['data']) + static record TextureRequest(int width, int height, ColourFormat format, ByteBuffer data) implements Request { + } + + /** + * Request the creation of a mesh from the graphics system. + */ + Future requestMesh(SpriteMeshRequest spriteMeshRequest) + + /** + * Request an existing shader by name. + */ + Future requestShader(ShaderRequest shaderRequest) + + /** + * Request the creation of a texture from the graphics system. + */ + Future requestTexture(TextureRequest textureRequest) +} diff --git a/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/graphics/GraphicsSystem.groovy b/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/graphics/GraphicsSystem.groovy index aad698f7..3fcaa2c2 100644 --- a/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/graphics/GraphicsSystem.groovy +++ b/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/graphics/GraphicsSystem.groovy @@ -35,19 +35,30 @@ import org.slf4j.Logger import org.slf4j.LoggerFactory import static org.lwjgl.glfw.GLFW.* +import java.util.concurrent.BlockingQueue +import java.util.concurrent.Callable +import java.util.concurrent.ExecutorService +import java.util.concurrent.Executors +import java.util.concurrent.Future +import java.util.concurrent.LinkedBlockingQueue + /** * Graphics system, creates a display which drives the rendering loop of drawing * graphics objects. * * @author Emanuel Rabina */ -class GraphicsSystem extends EngineSystem implements EventTarget { +class GraphicsSystem extends EngineSystem implements GraphicsRequests, EventTarget { private static final Logger logger = LoggerFactory.getLogger(GraphicsSystem) private final String windowTitle private final GraphicsConfiguration config private final InputEventStream inputEventStream + private final ExecutorService executorService = Executors.newCachedThreadPool() + private final BlockingQueue>> meshRequests = new LinkedBlockingQueue<>() + private final BlockingQueue>> shaderRequests = new LinkedBlockingQueue<>() + private final BlockingQueue>> textureRequests = new LinkedBlockingQueue<>() private OpenGLContext context private OpenGLCamera camera @@ -124,6 +135,67 @@ class GraphicsSystem extends EngineSystem implements EventTarget { return context.window } + /** + * Run through all of the queued requests for graphics resources. + * + * @param renderer + */ + void processRequests(GraphicsRenderer renderer) { + + if (meshRequests) { + meshRequests.drain().each { meshRequestAndPipe -> + def (meshRequest, pipe) = meshRequestAndPipe + pipe.add(renderer.createSpriteMesh(surface: meshRequest.surface())) + } + } + + if (shaderRequests) { + shaderRequests.drain().each { shaderRequestAndPipe -> + def (shaderRequest, pipe) = shaderRequestAndPipe + pipe.add(renderer.getShader(shaderRequest.name())) + } + } + + if (textureRequests) { + textureRequests.drain().each { textureRequestAndPipe -> + def (textureRequest, pipe) = textureRequestAndPipe + pipe.add(renderer.createTexture(textureRequest.width(), textureRequest.height(), + textureRequest.format(), textureRequest.data())) + } + } + } + + /** + * Load a request onto the given queue, returning a future of the requested + * resource. + */ + , V> Future queueRequest(R request, BlockingQueue>> requestQueue) { + + return executorService.submit({ -> + var pipe = new LinkedBlockingQueue(1) + requestQueue.add(new Tuple2(request, pipe)) + return pipe.take() + } as Callable) + } + + @Override + Future requestMesh(SpriteMeshRequest spriteMeshRequest) { + + return queueRequest(spriteMeshRequest, meshRequests) + } + + @Override + Future requestShader(ShaderRequest shaderRequest) { + + return queueRequest(shaderRequest, shaderRequests) + } + + @Override + Future requestTexture(TextureRequest textureRequest) { + + return queueRequest(textureRequest, textureRequests) + } + /** * Start the graphics system loop: creates a new window in which to render the * elements in the current scene, cleaning it all up when made to shut down. @@ -140,6 +212,7 @@ class GraphicsSystem extends EngineSystem implements EventTarget { var window = context.window window.relay(FramebufferSizeEvent, this) window.relay(WindowMaximizedEvent, this) + scene.window = window // Only do quick window mode switching on Windows - the macOS experience // is quite different from using the fullscreen button which assigns the @@ -160,6 +233,8 @@ class GraphicsSystem extends EngineSystem implements EventTarget { trigger(new WindowCreatedEvent(window)) new OpenGLRenderer(config, window).withCloseable { renderer -> + scene.graphicsRequestHandler = this + camera = new OpenGLCamera(window.renderResolution) camera.withCloseable { camera -> new ImGuiLayer(config, window, inputEventStream).withCloseable { imGuiLayer -> @@ -181,6 +256,8 @@ class GraphicsSystem extends EngineSystem implements EventTarget { window.toggleVsync() shouldToggleVsync = false } + + processRequests(renderer) pipeline.render() window.swapBuffers() window.pollEvents() diff --git a/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/graphics/pipeline/RenderPipeline.groovy b/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/graphics/pipeline/RenderPipeline.groovy index d33515ad..dd1ee0f1 100644 --- a/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/graphics/pipeline/RenderPipeline.groovy +++ b/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/graphics/pipeline/RenderPipeline.groovy @@ -62,7 +62,6 @@ class RenderPipeline implements AutoCloseable { final GraphicsRenderer renderer final ImGuiLayer imGuiLayer - final Scene scene final Camera camera private final Mesh fullScreenQuad @@ -85,7 +84,6 @@ class RenderPipeline implements AutoCloseable { this.renderer = renderer this.imGuiLayer = imGuiLayer - this.scene = scene this.camera = camera fullScreenQuad = renderer.createSpriteMesh( diff --git a/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/media/Image.groovy b/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/media/Image.groovy index e754e665..0a39ee26 100644 --- a/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/media/Image.groovy +++ b/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/media/Image.groovy @@ -22,6 +22,7 @@ import nz.net.ultraq.redhorizon.engine.graphics.Material import nz.net.ultraq.redhorizon.engine.graphics.Mesh import nz.net.ultraq.redhorizon.engine.graphics.Shader import nz.net.ultraq.redhorizon.engine.graphics.opengl.SpriteShader +import nz.net.ultraq.redhorizon.engine.resources.Resource import nz.net.ultraq.redhorizon.engine.scenegraph.Node import nz.net.ultraq.redhorizon.filetypes.ColourFormat import nz.net.ultraq.redhorizon.filetypes.ImageFile @@ -37,7 +38,7 @@ import java.nio.ByteBuffer * * @author Emanuel Rabina */ -class Image implements GraphicsElement, Node { +class Image implements Resource, GraphicsElement, Node { final int width final int height diff --git a/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/media/ImageLoader.groovy b/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/media/ImageLoader.groovy index 10903b6f..0c815174 100644 --- a/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/media/ImageLoader.groovy +++ b/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/media/ImageLoader.groovy @@ -16,39 +16,25 @@ package nz.net.ultraq.redhorizon.engine.media -import nz.net.ultraq.redhorizon.engine.graphics.GraphicsSystem -import nz.net.ultraq.redhorizon.engine.scenegraph.Scene import nz.net.ultraq.redhorizon.filetypes.ImageFile +import groovy.transform.InheritConstructors + /** * Load a single image into existing engines. * * @author Emanuel Rabina */ +@InheritConstructors class ImageLoader extends MediaLoader { - private final GraphicsSystem graphicsEngine - - /** - * Constructor, create a loader for an image file. - * - * @param imageFile - * @param scene - * @param graphicsEngine - */ - ImageLoader(ImageFile imageFile, Scene scene, GraphicsSystem graphicsEngine) { - - super(imageFile, scene) - this.graphicsEngine = graphicsEngine - } - @Override Image load() { def width = file.width def height = file.height media = new Image(file) - .scaleXY(graphicsEngine.window.renderResolution.calculateScaleToFit(width, height)) + .scaleXY(scene.window.renderResolution.calculateScaleToFit(width, height)) .translate(-width / 2, -height / 2) scene << media diff --git a/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/media/MediaLoader.groovy b/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/media/MediaLoader.groovy index 2dcc3efa..f6df6813 100644 --- a/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/media/MediaLoader.groovy +++ b/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/media/MediaLoader.groovy @@ -20,8 +20,8 @@ import nz.net.ultraq.redhorizon.engine.scenegraph.Scene import nz.net.ultraq.redhorizon.filetypes.ResourceFile /** - * An object that can be used to create media types from media files, and attach - * them to existing game engines. + * An object that can be used to create media types from resource files, and + * attach them to existing game engines. * * @param The type of file to load. * @param The type of media loaded from the file. @@ -39,7 +39,7 @@ abstract class MediaLoader { * @param file * @param scene */ - protected MediaLoader(F file, Scene scene) { + MediaLoader(F file, Scene scene) { this.file = file this.scene = scene diff --git a/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/scenegraph/Node.groovy b/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/scenegraph/Node.groovy index 8f106aef..e9a1d01a 100644 --- a/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/scenegraph/Node.groovy +++ b/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/scenegraph/Node.groovy @@ -16,6 +16,8 @@ package nz.net.ultraq.redhorizon.engine.scenegraph +import nz.net.ultraq.redhorizon.engine.scripting.Scriptable + import org.joml.Matrix4f import org.joml.Vector2f import org.joml.Vector3f @@ -26,7 +28,7 @@ import org.joml.primitives.Rectanglef * * @author Emanuel Rabina */ -trait Node implements Visitable { +trait Node implements SceneEvents, Scriptable, Visitable { final Vector3f position = new Vector3f() final Matrix4f transform = new Matrix4f() @@ -38,10 +40,14 @@ trait Node implements Visitable { visitor.visit(this) } + @Override + void onSceneAdded(Scene scene) { + } + /** * Scale this element by the given values. */ - Node scale(float x, float y, float z) { + T scale(float x, float y, float z) { transform.scale(x, y, z) bounds.scale(x, y, z) @@ -54,7 +60,7 @@ trait Node implements Visitable { * @param factor * @return */ - Node scaleXY(float factor) { + T scaleXY(float factor) { return scale(factor, factor, 1) } @@ -65,7 +71,7 @@ trait Node implements Visitable { * @param offset * @return */ - Node translate(Vector3f offset) { + T translate(Vector3f offset) { return translate(offset.x, offset.y, offset.z) } @@ -77,7 +83,7 @@ trait Node implements Visitable { * @param z * @return */ - Node translate(Vector2f xyOffset, float z = 0) { + T translate(Vector2f xyOffset, float z = 0) { return translate(xyOffset.x, xyOffset.y, z) } @@ -90,7 +96,7 @@ trait Node implements Visitable { * @param z * @return */ - Node translate(float x, float y, float z = 0) { + T translate(float x, float y, float z = 0) { transform.translate(x, y, z) bounds.translate(x, y) diff --git a/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/scenegraph/Scene.groovy b/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/scenegraph/Scene.groovy index dd943bce..62e454bb 100644 --- a/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/scenegraph/Scene.groovy +++ b/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/scenegraph/Scene.groovy @@ -16,6 +16,8 @@ package nz.net.ultraq.redhorizon.engine.scenegraph +import nz.net.ultraq.redhorizon.engine.graphics.GraphicsRequests +import nz.net.ultraq.redhorizon.engine.graphics.Window import nz.net.ultraq.redhorizon.events.EventTarget import java.util.concurrent.CopyOnWriteArrayList @@ -30,6 +32,11 @@ class Scene implements EventTarget, Visitable { private final List nodes = new CopyOnWriteArrayList<>() + @Delegate + GraphicsRequests graphicsRequestHandler + + Window window + /** * Allow visitors into the scene for traversal. * @@ -45,13 +52,12 @@ class Scene implements EventTarget, Visitable { /** * Add a node to this scene. - * - * @param node - * @return */ Scene addNode(Node node) { nodes << node + node.onSceneAdded(this) + node.script?.onSceneAdded(this) trigger(new NodeAddedEvent(node)) return this } @@ -66,6 +72,18 @@ class Scene implements EventTarget, Visitable { } } + /** + * Locate the first node in the scene that satisfies the given predicate. + * + * @param predicate + * @return + * The matching node, or {code null} if no node satisfies {@code predicate}. + */ + T findNode(Closure predicate) { + + return nodes.find(predicate) + } + /** * Overloads the {@code <<} operator to add elements to this scene. * diff --git a/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/scenegraph/Positionable.groovy b/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/scenegraph/SceneEvents.groovy similarity index 68% rename from redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/scenegraph/Positionable.groovy rename to redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/scenegraph/SceneEvents.groovy index a01e5c71..21921094 100644 --- a/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/scenegraph/Positionable.groovy +++ b/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/scenegraph/SceneEvents.groovy @@ -1,12 +1,12 @@ -/* - * Copyright 2019, Emanuel Rabina (http://www.ultraq.net.nz/) - * +/* + * Copyright 2024, Emanuel Rabina (http://www.ultraq.net.nz/) + * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -16,14 +16,16 @@ package nz.net.ultraq.redhorizon.engine.scenegraph -import org.joml.Vector3f - /** - * Trait for objects that can be placed in the world. - * + * Interface for all of the scene events a node will encounter during its + * lifetime in a scene. + * * @author Emanuel Rabina */ -trait Positionable { +interface SceneEvents { - Vector3f position = new Vector3f() + /** + * Called when this node is added to the scene. + */ + void onSceneAdded(Scene scene) } diff --git a/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/scenegraph/nodes/Sprite.groovy b/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/scenegraph/nodes/Sprite.groovy new file mode 100644 index 00000000..b415335e --- /dev/null +++ b/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/scenegraph/nodes/Sprite.groovy @@ -0,0 +1,88 @@ +/* + * Copyright 2024, Emanuel Rabina (http://www.ultraq.net.nz/) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package nz.net.ultraq.redhorizon.engine.scenegraph.nodes + +import nz.net.ultraq.redhorizon.engine.graphics.GraphicsElement +import nz.net.ultraq.redhorizon.engine.graphics.GraphicsRenderer +import nz.net.ultraq.redhorizon.engine.graphics.GraphicsRequests.ShaderRequest +import nz.net.ultraq.redhorizon.engine.graphics.GraphicsRequests.SpriteMeshRequest +import nz.net.ultraq.redhorizon.engine.graphics.GraphicsRequests.TextureRequest +import nz.net.ultraq.redhorizon.engine.graphics.Material +import nz.net.ultraq.redhorizon.engine.graphics.Mesh +import nz.net.ultraq.redhorizon.engine.graphics.Shader +import nz.net.ultraq.redhorizon.engine.graphics.opengl.SpriteShader +import nz.net.ultraq.redhorizon.engine.scenegraph.Node +import nz.net.ultraq.redhorizon.engine.scenegraph.Scene +import nz.net.ultraq.redhorizon.filetypes.ImageFile + +import org.joml.primitives.Rectanglef + +import groovy.transform.TupleConstructor + +/** + * A simple 2D sprite node. Contains a texture and coordinate data for what + * parts of that texture to render (ie: the texture represents a larger sprite + * sheet / texture atlas, so we need to know what part of that to render). + * + * @author Emanuel Rabina + */ +@TupleConstructor(includes = ['imageFile']) +class Sprite implements Node, GraphicsElement { + + final ImageFile imageFile + + private Mesh mesh + private Shader shader + private Material material + private Rectanglef region + + @Override + void delete(GraphicsRenderer renderer) { + } + + @Override + void init(GraphicsRenderer renderer) { + } + + @Override + void onSceneAdded(Scene scene) { + + var width = imageFile.width + var height = imageFile.height + var format = imageFile.format + + mesh = scene + .requestMesh(new SpriteMeshRequest(new Rectanglef(0, 0, width, height))) + .get() + shader = scene + .requestShader(new ShaderRequest(SpriteShader.NAME)) + .get() + material = new Material( + texture: scene + .requestTexture(new TextureRequest(width, height, format, imageFile.imageData.flipVertical(width, height, format))) + .get(), + transform: transform + ) + region = new Rectanglef(0, 0, width, height) + } + + @Override + void render(GraphicsRenderer renderer) { + + renderer.draw(mesh, shader, material) + } +} diff --git a/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/scenegraph/Selectable.groovy b/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/scenegraph/nodes/SpriteScript.groovy similarity index 55% rename from redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/scenegraph/Selectable.groovy rename to redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/scenegraph/nodes/SpriteScript.groovy index 2032540e..e16b55e0 100644 --- a/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/scenegraph/Selectable.groovy +++ b/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/scenegraph/nodes/SpriteScript.groovy @@ -1,12 +1,12 @@ -/* - * Copyright 2007, Emanuel Rabina (http://www.ultraq.net.nz/) - * +/* + * Copyright 2024, Emanuel Rabina (http://www.ultraq.net.nz/) + * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -14,22 +14,23 @@ * limitations under the License. */ -package nz.net.ultraq.redhorizon.engine.scenegraph +package nz.net.ultraq.redhorizon.engine.scenegraph.nodes + +import nz.net.ultraq.redhorizon.engine.scripting.Script /** - * Interface for elements that are selectable by some input in a scene. - * + * A script for sprites. Can be used over the standard {@link Script} class so + * that the sprite has a better name within scripts. + * * @author Emanuel Rabina */ -interface Selectable { +abstract class SpriteScript extends Script { /** - * Called when this element has been deselected by some action. + * Adds an alias of {@code sprite} to the script target. */ - void deselect() + protected Sprite getSprite() { - /** - * Called when this element has been selected by some action. - */ - void select() + return scriptable + } } diff --git a/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/scripting/Script.groovy b/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/scripting/Script.groovy new file mode 100644 index 00000000..a9394171 --- /dev/null +++ b/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/scripting/Script.groovy @@ -0,0 +1,44 @@ +/* + * Copyright 2024, Emanuel Rabina (http://www.ultraq.net.nz/) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package nz.net.ultraq.redhorizon.engine.scripting + +import nz.net.ultraq.redhorizon.engine.scenegraph.Scene +import nz.net.ultraq.redhorizon.engine.scenegraph.SceneEvents + +/** + * A script is a piece of code attached to an entity to customize its behaviour. + * + * @author Emanuel Rabina + */ +abstract class Script implements SceneEvents { + + protected T scriptable + + /** + * Attach the script target to this script, allowing it to act as a delegate + * within other script methods. + */ + Script attachScriptable(T scriptable) { + + this.scriptable = scriptable + return this + } + + @Override + void onSceneAdded(Scene scene) { + } +} diff --git a/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/scripting/Scriptable.groovy b/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/scripting/Scriptable.groovy new file mode 100644 index 00000000..cfe876eb --- /dev/null +++ b/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/scripting/Scriptable.groovy @@ -0,0 +1,40 @@ +/* + * Copyright 2024, Emanuel Rabina (http://www.ultraq.net.nz/) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package nz.net.ultraq.redhorizon.engine.scripting + +/** + * Any class that can have a script attached to customize its behaviour. + * + * @author Emanuel Rabina + */ +trait Scriptable { + + Script script + + /** + * Attach a script to this object to control its behaviour. Currently only + * supports 1 script per object. + */ + T attachScript(Script script) { + + if (this.script) { + throw new IllegalStateException('Script already attached to this node') + } + this.script = script.attachScriptable(this) + return this + } +} diff --git a/redhorizon-explorer/source/nz/net/ultraq/redhorizon/explorer/Explorer.groovy b/redhorizon-explorer/source/nz/net/ultraq/redhorizon/explorer/Explorer.groovy index 916bc03f..ab3a4c10 100644 --- a/redhorizon-explorer/source/nz/net/ultraq/redhorizon/explorer/Explorer.groovy +++ b/redhorizon-explorer/source/nz/net/ultraq/redhorizon/explorer/Explorer.groovy @@ -27,8 +27,8 @@ import nz.net.ultraq.redhorizon.engine.input.KeyEvent import nz.net.ultraq.redhorizon.engine.media.AnimationLoader import nz.net.ultraq.redhorizon.engine.media.ImageLoader import nz.net.ultraq.redhorizon.engine.media.ImagesLoader +import nz.net.ultraq.redhorizon.engine.media.MediaLoader import nz.net.ultraq.redhorizon.engine.media.Playable -import nz.net.ultraq.redhorizon.engine.media.ResourceLoader import nz.net.ultraq.redhorizon.engine.media.SoundLoader import nz.net.ultraq.redhorizon.engine.media.VideoLoader import nz.net.ultraq.redhorizon.filetypes.AnimationFile @@ -64,7 +64,7 @@ class Explorer extends Application { private File currentDirectory private InputStream selectedFileInputStream private Object selectedFile - private ResourceLoader selectedLoader + private MediaLoader selectedLoader private Palette palette /**