YUNPM - a Java Media Player

I don’t know about most people, but when I reach a video in a game (about 95% of the time) I’m hitting the escape key like a skinner rat try to get crack.

That doesn’t mean we, as developers, shouldn’t try :slight_smile:

I guess you dont play Metal Gear Solid or Final Fantasy games.
There are only few games which have a great elaborate story but no cut-scenes. BioShock was cool, but its more atmosphere and scenery than story and plot.

Sorry for offtopic, just always hits me, since I enjoyed 999 hours of video and 5 of gameplay in MGS4…

OT

I here this from some players… But the bulk of players really like cut scenes. People still go on about them in StarCraft 1. I felt ripped off in WC3 when they just zoom in to low poly characters in engine. Its no less “suspension of disbelief destroying” than a cut scene but somehow cheaper. One thing i don’t want to do… read large blocks of text.

Either way they should always be skippable.

They are pretty bad-ass. Overall SC’s story is as good as the gameplay.

Of course.

@Riven: agreed. I even considered writing a pure java codec, but then the patent minefield appears in my head and I slink away.

@Others: I’m not a typical player, but my real point is that if you’re considering video playback for cutscenes can you create cutscenes worth all of the effort?

It does specifically with FFMPEG - x264 is one of the key GPL components. I haven’t yet found a definitive list so don’t know what else that would be useful is not in the LGPL version.

Its a required skill for at least one in the team to have it, like level design, dialogue writing, making music and sound and so on.
No problem @ video editing and our 3D maya/3ds max skills only get better.
Our first game to have cutscenes will feature visual novel style cutscenes like Infamous with effects and all that. Much more than we could do only with only code, without a major headache…

Cutscenes in real time are always better. I hate these prerendered excuses for making an event system work. I never liked this about Final Fantasy. I was so amazed when I played Devil May Cry and they had these awesome cutscenes (was my fist PS2 game back then :D) and then FF 10… stupid videos :cranky:
Look at Uncharted how awesome in can be to actually PLAY a cutscene…

I don’t have something against such things as Comic Cutscenes as long as they don’t pop up every time a cutscene is triggered. It just feels lame when I play this awesome game and then a cutscenes comes and you always see the difference in quality.

I think it was awesome in Red Alert 2: a RTS game, with cutscenes after each level with real actors.

ztM6TGVNKJw

Theora is good enough for Blizzard to use it for cutscenes in Starcraft 2 and Diablo 3.

It isn’t only about cutscenes. You can play video on any surface in a 3D scene with this library.

Yup the C&C and Redalert games also had mission update FMV in-game. Some games have even used FMV’s as backgrounds during gameplay.

=

<3<3<3<3<3<3

YUNPM v0.8.1 is on it’s way, with a procedural API, allowing for trivial embedding in a 3D scene.

http://indiespot.net/files/medialib/indiespot-media-0.8.xx-screenshot-01.png

You are bloody awesome!

Version 0.8.1 - 0.8.3 of YUNPM

Changes
[x]Refactored callback API into procedural API
[x]Simplified API, making it trivial to embed in a 3D scene
[x]Added a simple sample sumple 3D scene as a proof of concept
[x]Removed VideoMetadata from public API (v0.8.2).
[x]Fixed bug determining 32-bit / 64-bit JVM (v0.8.3).

Download files
[x] http://indiespot.net/files/projects/medialib/

Code, code. (code)

/*
 * Copyright (c) 2012, Riven
 *
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 *     * Redistributions of source code must retain the above copyright notice,
 *       this list of conditions and the following disclaimer.
 *     * Redistributions in binary form must reproduce the above copyright
 *       notice, this list of conditions and the following disclaimer in the
 *       documentation and/or other materials provided with the distribution.
 *     * Neither the name of Riven nor the names of its contributors may
 *       be used to endorse or promote products derived from this software
 *       without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

package net.indiespot.media.test;

import static org.lwjgl.opengl.GL11.*;

import java.io.File;
import java.nio.ByteBuffer;

import net.indiespot.media.Movie;
import net.indiespot.media.impl.OpenALAudioRenderer;

import org.lwjgl.BufferUtils;
import org.lwjgl.LWJGLException;
import org.lwjgl.openal.AL;
import org.lwjgl.opengl.Display;
import org.lwjgl.opengl.DisplayMode;
import org.lwjgl.opengl.PixelFormat;
import org.lwjgl.util.glu.GLU;

import craterstudio.math.EasyMath;
import craterstudio.text.TextValues;

public class TestGameLoop {
	public static void main(String path) throws Exception {

		File movieFile = new File(path);
@@		Movie movie = Movie.open(movieFile);

		OpenALAudioRenderer audioRenderer = new OpenALAudioRenderer();
		audioRenderer.init(movie.audioStream(), movie.framerate());

		//

		Display.setDisplayMode(new DisplayMode(800, 600));
		Display.setResizable(true);
		Display.setTitle("TestGame");
		Display.setVSyncEnabled(false);

		// create display
		{
			for (int samples = 8; samples >= 0; samples--) {
				try {
					Display.create(new PixelFormat(/* bpp */32, /* alpha */8, /* depth */24,/* stencil */8,/* samples */4));
					break;
				} catch (LWJGLException exc) {
					System.out.println("Failed to create Display with " + samples + " samples");
				}
			}
		}

		// setup projection matrix
		{
			displayWidth = Display.getWidth();
			displayHeight = Display.getHeight();
			{
				glMatrixMode(GL_PROJECTION);
				glLoadIdentity();
				GLU.gluPerspective(60.0f, displayWidth / (float) displayHeight, 0.01f, 100.0f);

				glMatrixMode(GL_MODELVIEW);
				glLoadIdentity();

				glViewport(0, 0, displayWidth, displayHeight);
			}
		}

		// create texture holding video frame
		{
			textureHandle = glGenTextures();
			glBindTexture(GL_TEXTURE_2D, textureHandle);

			glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
			glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
			glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
			glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);

			int wPot = EasyMath.fitInPowerOfTwo(movie.width());
			int hPot = EasyMath.fitInPowerOfTwo(movie.height());
			texWidthUsedRatio = (float) movie.width() / wPot;
			texHeightUsedRatio = (float) movie.height() / hPot;

			// 'tmpbuf' should be null, but some drivers are too buggy
			ByteBuffer tmpbuf = BufferUtils.createByteBuffer(wPot * hPot * 3);
			glTexImage2D(GL_TEXTURE_2D, 0/* level */, GL_RGB, wPot, hPot, 0/* border */, GL_RGB, GL_UNSIGNED_BYTE, tmpbuf);
			tmpbuf = null;
		}

		// game loop

		long textureReceiveTook = 0;
		long textureUpdateTook = 0;
		long textureRenderTook = 0;

		long started = System.nanoTime();
		long startedLastSecond = System.nanoTime();
		int videoFramesLastSecond = 0;
		int renderFramesLastSecond = 0;

		while (!Display.isCloseRequested()) {

			// handle input
			{
				while (Keyboard.next()) {
					if (Keyboard.getEventKeyState()) { // on key press
						if (Keyboard.getEventKey() == Keyboard.KEY_SPACE) {
							if (audioRenderer.getState() == AudioRenderer.State.PLAYING) {
								audioRenderer.pause();
							} else if (audioRenderer.getState() == AudioRenderer.State.PAUSED) {
								audioRenderer.resume();
							}
						}
					} else if (Keyboard.getEventKey() == Keyboard.KEY_ESCAPE) {
						audioRenderer.stop();
					}
				}

				while (Mouse.next()) {
					if (Mouse.getDWheel() != 0) { // on scrollwheel rotate
						float volume = audioRenderer.getVolume();
						volume += (Mouse.getDWheel() > 0) ? +0.1f : -0.1f;
						volume = EasyMath.clamp(volume, 0.0f, 1.0f);
						audioRenderer.setVolume(volume);
					}

					if (Mouse.getEventButtonState()) { // on mouse-button press
						if (audioRenderer.getState() == AudioRenderer.State.PLAYING) {
							audioRenderer.pause();
						} else if (audioRenderer.getState() == AudioRenderer.State.PAUSED) {
							audioRenderer.resume();
						}
					}
				}
			}

@@			audioRenderer.tick(movie);

			glClearColor(0, 0, 0, 1);
			glClear(GL_COLOR_BUFFER_BIT);

			// position camera, make it sway
			{
				glMatrixMode(GL_MODELVIEW);
				{
					glLoadIdentity();

					long elapsed = (System.nanoTime() - started) / 1_000_000L;
					float angle = 90 + (float) Math.sin(elapsed * 0.001) * 15;

					// inverse camera transformations
					glRotatef(-angle, 0, 1, 0);
					glRotatef(-15f, 0, 0, 1);
					glTranslatef(-3, -1.7f, -0);
				}
			}

			glEnable(GL_TEXTURE_2D);
			glBindTexture(GL_TEXTURE_2D, textureHandle);

@@			if (movie.isTimeForNextFrame()) {
				// grab the next frame from the video stream
				textureReceiveTook = System.nanoTime();
@@				textureBuffer = movie.videoStream().readFrameInto(textureBuffer);
				textureReceiveTook = System.nanoTime() - textureReceiveTook;

				if (textureBuffer == null) {
					break;
				}

				textureUpdateTook = System.nanoTime();
				glTexSubImage2D(GL_TEXTURE_2D, 0/* level */, 0, 0, movie.width(), movie.height(), GL_RGB, GL_UNSIGNED_BYTE, textureBuffer);
				textureUpdateTook = System.nanoTime() - textureUpdateTook;

				// signal the AV-sync that we processed a frame
				movie.onUpdatedVideoFrame();

				videoFramesLastSecond++;
			}

			// render scene
			textureRenderTook = System.nanoTime();
			{
				glEnable(GL_BLEND);
				glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

				float h = (float) movie.height() / movie.width() * 2;

				glPushMatrix();

				glBegin(GL_QUADS);
				{
					glColor4f(1, 1, 1, 1);

					// render movie screen

					glTexCoord2f(0 * texWidthUsedRatio, 1 * texHeightUsedRatio);
					glVertex3f(0, 0.1f + h * 0, +1);

					glTexCoord2f(1 * texWidthUsedRatio, 1 * texHeightUsedRatio);
					glVertex3f(0, 0.1f + h * 0, -1);

					glTexCoord2f(1 * texWidthUsedRatio, 0 * texHeightUsedRatio);
					glVertex3f(0, 0.1f + h * 1, -1);

					glTexCoord2f(0 * texWidthUsedRatio, 0 * texHeightUsedRatio);
					glVertex3f(0, 0.1f + h * 1, +1);

					// radiosity / blur on the floor

					for (int i = -15; i <= +15; i++) {
						glColor4f(1, 1, 1, 0.025f);
						glTexCoord2f(0 * texWidthUsedRatio, 1 * texHeightUsedRatio);
						glVertex3f(0, 0, +1);

						glTexCoord2f(1 * texWidthUsedRatio, 1 * texHeightUsedRatio);
						glVertex3f(0, 0, -1);

						glColor4f(1, 1, 1, 0.0f);
						glTexCoord2f(1 * texWidthUsedRatio, 0 * texHeightUsedRatio);
						glVertex3f(0, -0.5f * h + Math.abs(i) * 0.01f, -1 + (i * 4) * 0.01f);

						glTexCoord2f(0 * texWidthUsedRatio, 0 * texHeightUsedRatio);
						glVertex3f(0, -0.5f * h + Math.abs(i) * 0.01f, +1 + (i * 4) * 0.01f);
					}
				}
				glEnd();
				glDisable(GL_BLEND);
				glDisable(GL_TEXTURE_2D);

				glPopMatrix();
			}
			textureRenderTook = System.nanoTime() - textureRenderTook;

			renderFramesLastSecond++;
			if (System.nanoTime() > startedLastSecond + 1_000_000_000L) {
				startedLastSecond += 1_000_000_000L;

				String a = TextValues.formatNumber(textureReceiveTook / 1_000_000.0, 1);
				String b = TextValues.formatNumber(textureUpdateTook / 1_000_000.0, 1);
				String c = TextValues.formatNumber(textureRenderTook / 1_000_000.0, 1);
				Display.setTitle(//
				   "rendering " + renderFramesLastSecond + "fps, " + //
				      "video " + videoFramesLastSecond + "fps, " + //
				      "read blocking: " + a + "ms, " + //
				      "texture update: " + b + "ms, " + //
				      "rendering: " + c + "ms");

				renderFramesLastSecond = 0;
				videoFramesLastSecond = 0;
			}

			Display.update();
		}

		movie.close();
		audioRenderer.close();

		Display.destroy();
		AL.destroy();

		System.exit(0);
	}

	static int displayWidth;
	static int displayHeight;

	static int textureHandle;
	static ByteBuffer textureBuffer;

	static float texWidthUsedRatio;
	static float texHeightUsedRatio;
}


WRT: My previous comment. I’m not attempt to suggest a given game shouldn’t…I’m simply saying carefully consider. All the time spent creating CG animations, music, lip-sync, voice recording (ugg)…is that the best way to improve the game (esp on a tight budget)? Would that time be better spent elsewhere? Learning experience certain has great value (if you can afford it) but is it also the best way to spent time learning? Important questions.

Absolutely. It’s just a thing that was not possible before, generally, easily, that is now possible.

We were going to include video in Revenge of the Titans but back in 2010 there was still no trivial solution to it. In the end we did the story screens as they are, and it was a lot of work and admittedly a better result, but there we go. Now in Ultratron we’ve got all these video displays in little monitors at the edge of the playfield, and Chaz has rendered every single frame out as png, making the final downloadable rather larger than it should be. If Riven’s solution is considerably smaller we might switch to using it.

Cas :slight_smile:

@Riven: just here to say great work ;D