Sunday, February 24, 2013

Conway's Game of Life using JavaFX 3D


I didn't need to change much of the code to run it in 3D - just replace rectangles with boxes, add camera and light - that's it.



What you'll get when using 3D features (in any language) is increased complexity. You have to think about the camera's position, it's field of view, the light sources, their position, the materials, reflections, textures ...

But: What is really impressive concerning JavaFX is that from the programmers viewpoint everything stays the same - for example, you can register your mouseOver actions on a Box in 3D the same way you can use it in 2D with Rectangles. The timelining works the same no matter if you animate 3D or 2D objects etc.

Here is the code for the video above (JDK8 needed!):


package net.ladstatt.apps
import scala.collection.JavaConversions.asScalaBuffer
import scala.collection.JavaConversions.seqAsJavaList
import scala.util.Random
import javafx.animation.Animation
import javafx.animation.KeyFrame
import javafx.animation.Timeline
import javafx.application.Application
import javafx.collections.ObservableList
import javafx.event.ActionEvent
import javafx.event.Event
import javafx.event.EventHandler
import javafx.scene.Group
import javafx.scene.Node
import javafx.scene.Scene
import javafx.scene.input.MouseEvent
import javafx.scene.paint.Color
import javafx.scene.paint.CycleMethod
import javafx.scene.paint.LinearGradient
import javafx.scene.paint.Stop
import javafx.scene.shape.Rectangle
import javafx.stage.Stage
import javafx.util.Duration
import javafx.scene.PerspectiveCamera
import javafx.scene.paint.PhongMaterial
import javafx.scene.PointLight
import javafx.scene.shape.Box
import javafx.scene.transform.Rotate
import javafx.geometry.Point3D
/**
* John Conway's Game of Life - with JavaFX and Scala using JDK8 3D API
*/
object GameOfLife {
def main(args: Array[String]): Unit = {
Application.launch(classOf[GameOfLife], args: _*)
}
}
trait JfxUtils {
def mkEventHandler[E <: Event](f: E => Unit) = new EventHandler[E] { def handle(e: E) = f(e) }
}
class GameOfLife extends javafx.application.Application with JfxUtils {
val canvasWidth = 800
val canvasHeight = 800
val canvasDepth = 800
val cellCount = 20
val lifeSpeed = 4
val gap = 4
// --------------------------------------------------------------------------
val (width, height, depth) = ((canvasWidth / cellCount) - gap, (canvasHeight / cellCount) - gap, (canvasDepth / cellCount) - gap)
var anchorX: Double = _
var anchorY: Double = _
var anchorAngle: Double = _
val pointLight = {
val l = new PointLight(Color.BISQUE)
l.setTranslateZ(-200)
l
}
val spectatorLight = {
val l = new PointLight(Color.AZURE)
l.setTranslateZ(-1500)
l
}
case class Cell(x: Int, y: Int, alive: Boolean = false) extends Box(width, height, depth) {
setUserData(alive)
setTranslateX(x * (width + gap))
setTranslateY(y * (height + gap))
paint()
setOnMousePressed(mkEventHandler((e: MouseEvent) => { killOrResurrect() }))
def paint() =
if (isAlive) {
setTranslateZ(depth * 2)
setMaterial(aliveMaterial)
} else {
setMaterial(deadMaterial)
setTranslateZ(0)
}
def isAlive: Boolean = getUserData().asInstanceOf[Boolean]
def killOrResurrect() = {
setUserData(!isAlive)
paint()
}
}
override def start(stage: Stage): Unit = {
stage.setTitle("Conway's Game of Life")
val cells = new Group((for {
x <- 0 to cellCount
y <- 0 to cellCount
} yield Cell(x, y, Random.nextBoolean)))
val drawingArea = new Group(cells, pointLight)
drawingArea.setTranslateZ(1500)
drawingArea.setRotationAxis(new Point3D(1, 1, 1))
val growTimeline = new Timeline
growTimeline.setRate(lifeSpeed)
growTimeline.setCycleCount(Animation.INDEFINITE)
growTimeline.getKeyFrames().add(
new KeyFrame(Duration.seconds(1),
new EventHandler[ActionEvent]() {
def handle(event: ActionEvent) {
val nextGen = nextGeneration((convert2CellList(cells.getChildren())))
cells.getChildren().clear()
cells.getChildren.addAll(nextGen)
}
}))
growTimeline.play()
val all = new Group(drawingArea, spectatorLight)
val scene = new Scene(all, canvasWidth, canvasHeight, true)
scene.setOnMouseMoved(mkEventHandler((e: MouseEvent) => {
pointLight.setTranslateX(e.getX)
pointLight.setTranslateY(e.getY)
}))
scene.setFill(Color.BLACK)
scene.setOnMousePressed(mkEventHandler((event: MouseEvent) => {
anchorX = event.getSceneX()
anchorAngle = drawingArea.getRotate()
}))
scene.setOnMouseDragged(mkEventHandler((event: MouseEvent) => {
drawingArea.setRotate(anchorAngle + anchorX - event.getSceneX())
}))
val perspectiveCamera = new PerspectiveCamera(false)
scene.setCamera(perspectiveCamera)
stage.setScene(scene)
stage.show()
}
def nextGeneration(allCells: List[Cell]): List[Cell] = {
for (c <- allCells) yield {
if (c.isAlive) {
getAliveNeighbors(c, allCells).size match {
case 1 => c.copy(alive = false)
case 2 => c
case 3 => c
case _ => c.copy(alive = false)
}
} else {
if (getAliveNeighbors(c, allCells).size == 3)
c.copy(alive = true)
else c
}
}
}
def getAliveNeighbors(c: Cell, cells: List[Cell]) = getNeighbors(c, cells).filter(_.isAlive)
def convert2CellList(nodes: ObservableList[Node]): List[Cell] = {
(for (
n <- nodes if (n match {
case c: Cell => true
case _ => false
})
) yield n.asInstanceOf[Cell]).toList
}
def getNeighbors(cell: Cell, cells: List[Cell]) =
cells.filter(c => c != cell && (scala.math.abs(cell.x - c.x) <= 1 && scala.math.abs(cell.y - c.y) <= 1))
def mkMaterial(color: Color): PhongMaterial = {
val m = new PhongMaterial()
m.setDiffuseColor(color)
m.setSpecularColor(Color.WHITESMOKE)
m
}
val aliveMaterial = mkMaterial(Color.RED)
val deadMaterial = mkMaterial(Color.WHITE)
}

JavaFX 3D Hello World

To compile JavaFX with 3D features you have to get the early access version of the JDK8.

At the moment as far as I know there is only a windows support for the 3D features, but a build for Mac and Linux will soon be released. (Luckily enough 3D support also works for a virtualized Windows running on Mac - this is how i got to the screeenshots.)

This blog entry is about a Scala version of the provided 3D examples

First, there is the class PhongMaterial, which defines some sort of "Phong shaded material". Basically you can create a material which can have a color or some texture. 

a red box and a blue sphere rendered with JavaFX

This is a screenshot of the same program, different colors, with a bumpmap applied:

example using a bump map
Here is the code:


package net.ladstatt.apps
/*
* Copyright (c) 2013, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
import javafx.application.Application
import javafx.geometry.Point3D
import javafx.scene.Group
import javafx.scene.PerspectiveCamera
import javafx.scene.PointLight
import javafx.scene.Scene
import javafx.scene.input.MouseEvent
import javafx.scene.paint.Color
import javafx.scene.paint.PhongMaterial
import javafx.scene.shape.Box
import javafx.scene.shape.Sphere
import javafx.stage.Stage
import javafx.scene.image.Image
/**
* original source from here https://wikis.oracle.com/display/OpenJDK/SphereAndBox.java
**/
object SphereAndBox {
def main(args: Array[String]): Unit = {
System.setProperty("prism.dirtyopts", "false")
Application.launch(classOf[SphereAndBox], args: _*)
}
}
class SphereAndBox extends javafx.application.Application with JfxUtils {
var anchorX: Double = _
var anchorY: Double = _
var anchorAngle: Double = _
val bumpMap = new Image(getClass.getResourceAsStream("/jfx.png"))
def addCamera(scene: Scene): PerspectiveCamera = {
val perspectiveCamera = new PerspectiveCamera(false)
scene.setCamera(perspectiveCamera)
perspectiveCamera
}
override def start(primaryStage: Stage) {
primaryStage.setTitle("SphereAndBox")
val boxMaterial = new PhongMaterial()
boxMaterial.setDiffuseColor(Color.GREEN)
boxMaterial.setSpecularColor(Color.WHITESMOKE)
val sphereMaterial = new PhongMaterial()
sphereMaterial.setDiffuseColor(Color.BISQUE)
sphereMaterial.setSpecularColor(Color.LIGHTBLUE)
sphereMaterial.setBumpMap(bumpMap)
val box = new Box(400, 400, 400)
box.setMaterial(boxMaterial)
val sphere = new Sphere(200)
sphere.setMaterial(sphereMaterial)
sphere.setTranslateX(250)
sphere.setTranslateY(250)
sphere.setTranslateZ(50)
box.setTranslateX(250)
box.setTranslateY(250)
box.setTranslateZ(450)
val parent = new Group(box, sphere)
parent.setTranslateZ(500)
parent.setRotationAxis(new Point3D(1, 1, 1))
val root = new Group(parent)
val scene = new Scene(root, 500, 500, true)
scene.setOnMousePressed(mkEventHandler((event: MouseEvent) => {
anchorX = event.getSceneX()
anchorY = event.getSceneY()
anchorAngle = parent.getRotate()
}))
scene.setOnMouseDragged(mkEventHandler((event: MouseEvent) => {
parent.setRotate(anchorAngle + anchorX - event.getSceneX())
}))
val pointLight = new PointLight(Color.ANTIQUEWHITE)
pointLight.setTranslateX(15)
pointLight.setTranslateY(-10)
pointLight.setTranslateZ(-100)
root.getChildren().add(pointLight)
addCamera(scene)
primaryStage.setScene(scene)
primaryStage.show()
}
}


With a little imagination you can surely think of many ways to use this features in your applications. At the moment PhongMaterial is the only implementation of the abstract Material class. 

In the above example, Sphere and Box classes are used to represent 3D shapes, but there are also other primitives provided, like Cylinder or MeshView.

You may also want to peek into the sources on the openjfx repository:

hg clone http://hg.openjdk.java.net/openjfx/8/graphics/rt/

Thursday, February 7, 2013

Conways Game of Life

I always wanted to implement Conways Game of Life, and by using JavaFX and Scala this happens to be possible with just some 140 lines of code.



Background information for the idea can be read on the wikipedia, I'll quote the important stuff here:


  1. Any live cell with fewer than two live neighbours dies, as if caused by under-population.
  2. Any live cell with two or three live neighbours lives on to the next generation.
  3. Any live cell with more than three live neighbours dies, as if by overcrowding.
  4. Any dead cell with exactly three live neighbours becomes a live cell, as if by reproduction.


This, expressed in Scala code, can be written as:


Conway's next generation algorithm

The funny thing is that there exist some starting configurations which produce some stable, self repeating patterns.

Here is the source:


package net.ladstatt.apps
import scala.collection.JavaConversions.asScalaBuffer
import scala.collection.JavaConversions.seqAsJavaList
import scala.util.Random
import javafx.animation.Animation
import javafx.animation.KeyFrame
import javafx.animation.Timeline
import javafx.application.Application
import javafx.collections.ObservableList
import javafx.event.ActionEvent
import javafx.event.Event
import javafx.event.EventHandler
import javafx.scene.Group
import javafx.scene.Node
import javafx.scene.Scene
import javafx.scene.input.MouseEvent
import javafx.scene.paint.Color
import javafx.scene.paint.CycleMethod
import javafx.scene.paint.LinearGradient
import javafx.scene.paint.Stop
import javafx.scene.shape.Rectangle
import javafx.stage.Stage
import javafx.util.Duration
/**
* John Conway's Game of Life - with JavaFX and Scala
*/
object GameOfLife {
def main(args: Array[String]): Unit = {
Application.launch(classOf[GameOfLife], args: _*)
}
}
trait JfxUtils {
def mkEventHandler[E <: Event](f: E => Unit) = new EventHandler[E] { def handle(e: E) = f(e) }
}
class GameOfLife extends javafx.application.Application with JfxUtils {
val canvasWidth = 800
val canvasHeight = 600
val cellCount = 40
val lifeSpeed = 2
// --------------------------------------------------------------------------
val (width, height) = ((canvasWidth / cellCount) - 2, (canvasHeight / cellCount) - 2)
case class Cell(x: Int, y: Int, alive: Boolean = false) extends Rectangle {
setUserData(alive)
setX(x * (width + 2))
setY(y * (height + 2))
setWidth(width)
setHeight(height)
setArcWidth(2)
setArcHeight(2)
paint
setOnMousePressed(mkEventHandler((e: MouseEvent) => { killOrResurrect }))
def paint = if (isAlive) setFill(Color.RED) else setFill(Color.WHITESMOKE)
def isAlive: Boolean = getUserData().asInstanceOf[Boolean]
def killOrResurrect = {
setUserData(!isAlive)
paint
}
}
override def start(stage: Stage): Unit = {
stage.setTitle("Conway's Game of Life")
val drawingArea = new Group()
val background = {
val b = new Rectangle(0, 0, canvasWidth, canvasHeight)
val stops = List(new Stop(0, Color.BLACK), new Stop(1, Color.WHITESMOKE))
val g = new LinearGradient(0.0, 1.0, 0.0, 0.0, true, CycleMethod.NO_CYCLE, stops)
b.setFill(g)
b
}
val cells = new Group()
cells.getChildren().addAll(for {
x <- 0 to cellCount
y <- 0 to cellCount
} yield Cell(x, y, Random.nextBoolean))
drawingArea.getChildren.addAll(background, cells)
val growTimeline = new Timeline
growTimeline.setRate(lifeSpeed)
growTimeline.setCycleCount(Animation.INDEFINITE)
growTimeline.getKeyFrames().add(
new KeyFrame(Duration.seconds(1),
new EventHandler[ActionEvent]() {
def handle(event: ActionEvent) {
val nextGen = nextGeneration((convert2CellList(cells.getChildren())))
cells.getChildren().clear()
cells.getChildren.addAll(nextGen)
}
}))
growTimeline.play()
stage.setScene(new Scene(drawingArea, canvasWidth, canvasHeight))
stage.show()
}
def nextGeneration(allCells: List[Cell]): List[Cell] = {
for (c <- allCells) yield {
if (c.isAlive) {
getAliveNeighbors(c, allCells).size match {
case 1 => c.copy(alive = false)
case 2 => c
case 3 => c
case _ => c.copy(alive = false)
}
} else {
if (getAliveNeighbors(c, allCells).size == 3)
c.copy(alive = true)
else c
}
}
}
def getAliveNeighbors(c: Cell, cells: List[Cell]) = getNeighbors(c, cells).filter(_.isAlive)
def convert2CellList(nodes: ObservableList[Node]): List[Cell] = {
(for (
n <- nodes if (n match {
case c: Cell => true
case _ => false
})
) yield n.asInstanceOf[Cell]).toList
}
def getNeighbors(cell: Cell, cells: List[Cell]) =
cells.filter(c => c != cell && (scala.math.abs(cell.x - c.x) <= 1 && scala.math.abs(cell.y - c.y) <= 1))
}


Tuesday, February 5, 2013

Tree visualization Part 3

In this post I want to make the trees more realistic and give them some leaves.

The improved realism can be achieved with a technique called "midpoint replacement", which was also used to make the lightning bolts look somewhat chaotic yet surprisingly realistic.

Of course, in the case of visualizing natural trees there are many factors to consider, and by applying unspecific random based algorithms you only reach a limited realism. But compared to the first approach using straight lines the results are much better ;-)

During the implementation of the lightning article, I've developed a datastructure which I wanted to use also for the plant-some-trees repository. This datastructure, "Vec", is shown below:

Vec datastructure

It contains some helper methods, which make it easier to do 2D calculations. Using this datastructure the midpoint replacement can be implemented like this:

Midpoint replacement algorithm
First, I refactored the code to use the Vec class, secondly, I added midpoint replacement for the trees.

To make the visualization even more interesting, I also added some leaves to the trees. I used a simple approach: If a branch is thin enough, it will get some leaves. Only the  traverse function had to be enhanced for that.

Result: a tree with leaves
The code is available at github.