Skip to content

Commit

Permalink
Create an animation node w/ the new scripting and node API
Browse files Browse the repository at this point in the history
  • Loading branch information
ultraq committed Feb 11, 2024
1 parent f92fd39 commit f482c9e
Show file tree
Hide file tree
Showing 12 changed files with 280 additions and 86 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import org.slf4j.LoggerFactory

import groovy.transform.TupleConstructor
import java.nio.ByteBuffer
import java.text.DecimalFormat

/**
* Implementation of the WSA file format as used in Tiberium Dawn and Red Alert.
Expand Down Expand Up @@ -146,7 +147,7 @@ class WsaFile implements AnimationFile, Streaming {

return [
"WSA file (C&C), ${width}x${height}, ${palette ? '18-bit w/ 256 colour palette' : '(no palette)'}",
"Contains ${numFrames} frames to run at ${String.format('%.2f', frameRate)}fps"
"Contains ${numFrames} frames to run at ${new DecimalFormat('0.#').format(frameRate)}fps"
].join(', ')
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,11 @@ 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.scenegraph.Node
import nz.net.ultraq.redhorizon.engine.scenegraph.nodes.Animation
import nz.net.ultraq.redhorizon.engine.scenegraph.nodes.FullScreenContainer
import nz.net.ultraq.redhorizon.engine.scenegraph.nodes.Sound
import nz.net.ultraq.redhorizon.engine.scenegraph.nodes.Sprite
import nz.net.ultraq.redhorizon.filetypes.AnimationFile
import nz.net.ultraq.redhorizon.filetypes.ImageFile
import nz.net.ultraq.redhorizon.filetypes.Palette
import nz.net.ultraq.redhorizon.filetypes.ResourceFile
Expand Down Expand Up @@ -71,9 +73,14 @@ class MediaPlayer extends Application {
logger.info('File details: {}', mediaFile)

var mediaNode = switch (mediaFile) {
case ImageFile -> new FullScreenContainer().addChild(new Sprite(mediaFile))
case SoundFile -> new Sound(mediaFile).attachScript(new SoundPlaybackScript())
default -> throw new UnsupportedOperationException("No media script for the associated file class of ${mediaFile}")
case ImageFile ->
new FullScreenContainer().addChild(new Sprite(mediaFile))
case AnimationFile ->
new FullScreenContainer().addChild(new Animation(mediaFile).attachScript(new PlaybackScript(this, true)))
case SoundFile ->
new Sound(mediaFile).attachScript(new PlaybackScript(this, mediaFile.forStreaming))
default ->
throw new UnsupportedOperationException("No media script for the associated file class of ${mediaFile}")
}

scene << mediaNode
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,37 +16,65 @@

package nz.net.ultraq.redhorizon.cli.mediaplayer

import nz.net.ultraq.redhorizon.engine.Application
import nz.net.ultraq.redhorizon.engine.input.KeyControl
import nz.net.ultraq.redhorizon.engine.media.Playable
import nz.net.ultraq.redhorizon.engine.media.StopEvent
import nz.net.ultraq.redhorizon.engine.scenegraph.Scene
import nz.net.ultraq.redhorizon.engine.scenegraph.nodes.Sound
import nz.net.ultraq.redhorizon.engine.scenegraph.scripting.Script

import org.slf4j.Logger
import org.slf4j.LoggerFactory
import static org.lwjgl.glfw.GLFW.GLFW_KEY_SPACE

import groovy.transform.TupleConstructor

/**
* A script to control playback of a sound effect or track.
*
* @author Emanuel Rabina
*/
class SoundPlaybackScript extends Script<Sound> {
@TupleConstructor
class PlaybackScript extends Script {

private static final Logger logger = LoggerFactory.getLogger(PlaybackScript)

final Application application
final boolean runOnce

@Delegate
private Sound applyDelegate() {
return scriptable
private Playable applyDelegate() {
return scriptable as Playable
}

@Override
void onSceneAdded(Scene scene) {

scene.inputEventStream.addControl(new KeyControl(GLFW_KEY_SPACE, 'Play/Pause', { ->
if (!playing || paused) {
play()
if (runOnce) {
logger.debug('Pausing/Resuming playback')
scene.gameClock.togglePause()
}
else if (!paused) {
pause()
else {
if (!playing || paused) {
logger.debug('Playing')
play()
}
}
}))

Thread.sleep(1000)
logger.debug('Beginning playback')
play()

on(StopEvent) { event ->
if (runOnce) {
logger.debug('Playback complete and script configured for runOnce behaviour - shutting down')
application.stop()
}
else {
logger.debug('Playback complete')
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ class OpenGLMesh extends Mesh {
var textureUV = textureUVs[index]
vertexBuffer.put(textureUV.x, textureUV.y)
}
default -> throw new IllegalArgumentException("Unhandled vertex layout part ${layoutPart.name()}")
default -> throw new UnsupportedOperationException("Unhandled vertex layout part ${layoutPart.name()}")
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,19 +104,4 @@ class Node<T extends Node> implements SceneEvents, Scriptable<T>, Visitable {

addChild(child)
}

/**
* Default implementation of the scene added event to notify any attached
* script, then this node's children.
*
* @param scene
*/
@Override
void onSceneAdded(Scene scene) {

script?.onSceneAdded(scene)
children.each { child ->
child.onSceneAdded(scene)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import nz.net.ultraq.redhorizon.engine.audio.AudioRequests
import nz.net.ultraq.redhorizon.engine.graphics.GraphicsRequests
import nz.net.ultraq.redhorizon.engine.graphics.Window
import nz.net.ultraq.redhorizon.engine.input.InputEventStream
import nz.net.ultraq.redhorizon.engine.time.GameClock
import nz.net.ultraq.redhorizon.events.EventTarget

import java.util.concurrent.CopyOnWriteArrayList
Expand All @@ -38,6 +39,7 @@ class Scene implements EventTarget, Visitable {
AudioRequests audioRequestsHandler
@Delegate
GraphicsRequests graphicsRequestHandler
GameClock gameClock
// TODO: A better name for this or way for nodes to have access to inputs?
InputEventStream inputEventStream

Expand All @@ -62,9 +64,25 @@ class Scene implements EventTarget, Visitable {
Scene addNode(Node node) {

nodes << node
addNodeAndChildren(node)
return this
}

/**
* Trigger the {@code onSceneAdded} event for this node and all its children.
* Each node triggers a {@link NodeAddedEvent} event.
*
* @param node
*/
private void addNodeAndChildren(Node node) {

node.onSceneAdded(this)
node.script?.onSceneAdded(this)
trigger(new NodeAddedEvent(node))
return this

node.children.each { childNode ->
addNodeAndChildren(childNode)
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
/*
* 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.Texture
import nz.net.ultraq.redhorizon.engine.graphics.opengl.SpriteShader
import nz.net.ultraq.redhorizon.engine.media.Playable
import nz.net.ultraq.redhorizon.engine.scenegraph.Node
import nz.net.ultraq.redhorizon.engine.scenegraph.Scene
import nz.net.ultraq.redhorizon.engine.time.Temporal
import nz.net.ultraq.redhorizon.filetypes.AnimationFile
import nz.net.ultraq.redhorizon.filetypes.Streaming
import nz.net.ultraq.redhorizon.filetypes.StreamingFrameEvent

import java.nio.ByteBuffer
import java.util.concurrent.Executors

/**
* An animation to play from a series of images/frames.
*
* @author Emanuel Rabina
*/
class Animation extends Node<Animation> implements GraphicsElement, Playable, Temporal {

final AnimationFile animationFile

private final List<Texture> frames = []
private long startTimeMs
private int currentFrame = -1
private int lastFrame = -1
private Mesh mesh
private Shader shader
private Material material

Animation(AnimationFile animationFile) {

bounds
.set(0, 0, animationFile.width, animationFile.forVgaMonitors ? animationFile.height * 1.2f as float : animationFile.height)
.center()
this.animationFile = animationFile
}

@Override
void delete(GraphicsRenderer renderer) {
}

@Override
void init(GraphicsRenderer renderer) {
}

@Override
void onSceneAdded(Scene scene) {

mesh = scene
.requestCreateOrGet(new SpriteMeshRequest(bounds))
.get()
shader = scene
.requestCreateOrGet(new ShaderRequest(SpriteShader.NAME))
.get()
material = new Material()

var width = animationFile.width
var height = animationFile.height
var format = animationFile.format

var requestFrame = { ByteBuffer frameData ->
return scene
.requestCreateOrGet(new TextureRequest(width, height, format, frameData.flipVertical(width, height, format)))
.get()
}

if (animationFile instanceof Streaming) {
var decoder = animationFile.streamingDecoder
decoder.on(StreamingFrameEvent) { event ->
frames << requestFrame(event.frame)
}
Executors.newVirtualThreadPerTaskExecutor().execute(decoder)
}
else {
Executors.newVirtualThreadPerTaskExecutor().execute { ->
animationFile.frameData.each { frame ->
frames << requestFrame(frame)
}
}
}
}

@Override
void onSceneRemoved(Scene scene) {

scene.requestDelete(mesh, shader, *frames)
}

@Override
void render(GraphicsRenderer renderer) {

if (mesh && shader && material && currentFrame != -1) {

// Draw the current frame if available
var currentFrameTexture = frames[currentFrame]
if (currentFrameTexture) {
material.texture = currentFrameTexture
renderer.draw(mesh, getGlobalTransform(), shader, material)
}

// Delete used frames as the animation progresses to free up memory
// NOTE: This basically ties this class to play-once streaming animations only 🤔
if (currentFrame > 0) {
for (var i = lastFrame; i < currentFrame; i++) {
renderer.deleteTexture(frames[i])
frames[i] = null
}
}
lastFrame = currentFrame
}
}

@Override
void tick(long updatedTimeMs) {

if (playing) {
Temporal.super.tick(updatedTimeMs)

if (!startTimeMs) {
startTimeMs = currentTimeMs
}

var nextFrame = Math.floor((currentTimeMs - startTimeMs) / 1000 * animationFile.frameRate) as int
if (nextFrame < animationFile.numFrames) {
currentFrame = nextFrame
}
else {
stop()
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,6 @@ class FullScreenContainer extends Node<FullScreenContainer> implements GraphicsE
}

addChild(new Outline())
super.onSceneAdded(scene)
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,6 @@ class Outline extends Node<Outline> implements GraphicsElement {
shader = scene
.requestCreateOrGet(new ShaderRequest(PrimitivesShader.NAME))
.get()

super.onSceneAdded(scene)
}

@Override
Expand Down
Loading

0 comments on commit f482c9e

Please sign in to comment.