P3D Cube Renderer with JavaFX Controls

Hello folks!

I was having some fun with Processing and JavaFX integration.

This is part of development for some future projects and still in it’s early stages!

Code < Click here to expand!
/**
 * Background P3D Cube Renderer with JavaFX Controls
 * Author: glv (primary developer and prompt engineer)
 * Assisted by: ChatGPT
 * Date: 2025-07-01
*  Version 1.0.0
 *
 * Description:
 * This Processing sketch demonstrates running a 3D rotating cube
 * on an offscreen PGraphics (P3D) inside a background PApplet,
 * while displaying the live rendered image in a JavaFX ImageView.
 * JavaFX sliders control the cube's yaw (Y axis), pitch (X axis),
 * and roll (Z axis) rotations in real-time.
 *
 * JavaFX UI runs on the JavaFX Application Thread via Platform.runLater(),
 * ensuring toolkit initialization and thread safety.
 * The cube renderer runs independently on a Processing thread.
 *
 * Environment:
 *   Processing 4.4.4
 *   Windows 10
 *   JavaFX 4.0 beta 5 library
 *
 * Note:
 *   For W10 and Processing 4.4.4 the JavaFX JAR files
 *   need to be placed in the sketch’s 'code' folder
 *   to enable JavaFX functionality.
 */

// Processing JavaFX integration to bridge Processing sketches with JavaFX
import processing.javafx.*;

// JavaFX application toolkit for thread management and UI updates
import javafx.application.Platform;

// JavaFX classes for scene management and UI components
import javafx.scene.Scene;
import javafx.scene.image.*;       // ImageView, WritableImage, PixelWriter, PixelFormat
import javafx.scene.control.*;    // Slider, Button, Label
import javafx.scene.layout.*;     // VBox layout container
import javafx.stage.Stage;        // Top-level JavaFX window
import javafx.geometry.Pos;       // Layout alignment constants

CubeRendererApp cubeApp;
ImageView imageView;
Slider yawSlider, pitchSlider, rollSlider;
Label yawLabel, pitchLabel, rollLabel;
Button startButton, resetButton;

volatile PImage flag;

void settings() {
  size(800, 600);
}

void setup() {
  System.setProperty("glass.win.uiScale", "1"); // This works!
   
//  flag = loadImage("https://upload.wikimedia.org/wikipedia/commons/thumb/d/d9/Flag_of_Canada_%28Pantone%29.svg/500px-Flag_of_Canada_%28Pantone%29.svg.png");

  surface.setVisible(false);

  // Force JavaFX toolkit initialization once:
  try {
    Platform.startup(() -> {
    }
    );
  }
  catch (IllegalStateException e) {
    // toolkit already initialized, ignore
  }

  // Now safely run JavaFX UI creation
  Platform.runLater(() -> {
    imageView = new ImageView();
    imageView.setFitWidth(300);
    imageView.setFitHeight(300);
    imageView.setPreserveRatio(true);

    // SLIDERS
    yawSlider = new Slider(-180, 180, 0);
    yawSlider.setShowTickMarks(true);
    yawSlider.setShowTickLabels(true);
    yawSlider.setMajorTickUnit(90);
    yawSlider.setMinorTickCount(2);
    yawSlider.setBlockIncrement(5);
    yawSlider.setSnapToTicks(false);
    yawSlider.setPadding(new javafx.geometry.Insets(0, 10, 0, 10));

    pitchSlider = new Slider(-180, 180, 0);
    pitchSlider.setShowTickMarks(true);
    pitchSlider.setShowTickLabels(true);
    pitchSlider.setMajorTickUnit(90);
    pitchSlider.setMinorTickCount(2);
    pitchSlider.setBlockIncrement(5);
    pitchSlider.setSnapToTicks(false);
    pitchSlider.setPadding(new javafx.geometry.Insets(0, 10, 0, 10));

    rollSlider = new Slider(-180, 180, 0);
    rollSlider.setShowTickMarks(true);
    rollSlider.setShowTickLabels(true);
    rollSlider.setMajorTickUnit(90);
    rollSlider.setMinorTickCount(2);
    rollSlider.setBlockIncrement(5);
    rollSlider.setSnapToTicks(false);
    rollSlider.setPadding(new javafx.geometry.Insets(0, 10, 0, 10));

    // Spacer
    Region spacer = new Region();
    spacer.setMinHeight(20); // or whatever height you want (e.g., 10–50 px)

    // LABELS
    yawLabel = new Label("Yaw (Y axis): 0°");
    pitchLabel = new Label("Pitch (X axis): 0°");
    rollLabel = new Label("Roll (Z axis): 0°");

    // BUTTONS
    startButton = new Button("Start Renderer");
    resetButton = new Button("Reset Rotations");

    startButton.setOnAction(e -> {
      if (cubeApp == null) {
        cubeApp = new CubeRendererApp();
        PApplet.runSketch(new String[]{"CubeRendererApp"}, cubeApp);
        bindSliders();
      }
    }
    );

    resetButton.setOnAction(e -> {
      if (cubeApp != null) {
        yawSlider.setValue(0);
        pitchSlider.setValue(0);
        rollSlider.setValue(0);
      }
    }
    );

    // LAYOUT (only declared once)
    VBox root = new VBox(10,
      imageView,
      spacer,   
      yawLabel, yawSlider,
      pitchLabel, pitchSlider,
      rollLabel, rollSlider,
      startButton,
      resetButton
      );
    root.setAlignment(Pos.CENTER);

    Stage stage = new Stage();
    stage.setScene(new Scene(root, 400, 730));
    stage.setTitle("Background P3D Cube with JavaFX Controls");
    stage.show();
  }
  );
}

void draw() {
  background(30);
  fill(255);
  textSize(18);
  text("Main sketch", 20, 40);

  // Update JavaFX ImageView with the latest cube frame
  if (cubeApp != null && cubeApp.sharedCopy != null) {
    WritableImage fxImg = pImageToWritableImage(cubeApp.sharedCopy);
    Platform.runLater(() -> imageView.setImage(fxImg));
  }
}

// Bind JavaFX sliders to update cube rotation angles
void bindSliders() {
  yawSlider.valueProperty().addListener((obs, oldV, newV) -> {
    float v = newV.floatValue();
    yawLabel.setText(String.format("Yaw (Y axis): %.2f", v));
    cubeApp.setYaw(v);
  }
  );
  pitchSlider.valueProperty().addListener((obs, oldV, newV) -> {
    float v = newV.floatValue();
    pitchLabel.setText(String.format("Pitch (X axis): %.2f", v));
    cubeApp.setPitch(v);
  }
  );
  rollSlider.valueProperty().addListener((obs, oldV, newV) -> {
    float v = newV.floatValue();
    rollLabel.setText(String.format("Roll (Z axis): %.2f", v));
    cubeApp.setRoll(v);
  }
  );
}

// Convert Processing PImage to JavaFX WritableImage for display
WritableImage pImageToWritableImage(PImage pImg) {
  pImg.loadPixels();
  WritableImage wimg = new WritableImage(pImg.width, pImg.height);
  PixelWriter pw = wimg.getPixelWriter();
  pw.setPixels(0, 0, pImg.width, pImg.height,
    javafx.scene.image.PixelFormat.getIntArgbInstance(),
    pImg.pixels, 0, pImg.width);
  return wimg;
}

// Background Processing PApplet rendering a rotating 3D cube offscreen
public static class CubeRendererApp extends PApplet {
  PGraphics pg;
  public volatile PImage sharedCopy;

  float yaw=0, pitch=0, roll=0;
  
  PImage flag;

  public void settings() {
    size(300, 300, P3D);
  }

  public void setup() {
    pg = createGraphics(300, 300, P3D);

    //surface.setVisible(false); Must be visible so will hide it!
    surface.setLocation(-500, -500);
    
    flag = loadImage("https://upload.wikimedia.org/wikipedia/commons/thumb/d/d9/Flag_of_Canada_%28Pantone%29.svg/500px-Flag_of_Canada_%28Pantone%29.svg.png");
    flag.resize(80, 180); 
    flag.save("test.png"); 
  }

  public void draw() {
    
   // // Position quad exactly on top face
    float w = 40;  // half width
    float d = 90;  // half depth
    float y = -10.1; // top face y coordinate (half height, negative because up)
   
   pg.beginDraw();
    pg.background(10);
    pg.lights();
    pg.translate(pg.width/2f, pg.height/2f, 0);
    pg.rotateY(radians(yaw));
    pg.rotateX(radians(pitch));
    pg.rotateZ(radians(roll));
    
    // Draw textured top face first
   pg.beginShape(QUADS);
    pg.texture(flag);

    // Rotate UVs -90° (counter-clockwise)
    pg.vertex(-w, y, -d, flag.width, 0);              // top-left → top-right
    pg.vertex( w, y, -d, flag.width, flag.height);    // top-right → bottom-right
    pg.vertex( w, y,  d, 0, flag.height);             // bottom-right → bottom-left
    pg.vertex(-w, y,  d, 0, 0);                       // bottom-left → top-left
    
   pg.endShape();
    
    // Then draw cube box
    pg.noFill();
    pg.stroke(255);
    pg.box(2*40, 2*10, 2*90);
    
   pg.endDraw();


    // Update shared frame for main sketch display
    sharedCopy = pg.get();
  }

  public void setYaw(float y) {
    yaw = y;
  }
  public void setPitch(float p) {
    pitch = p;
  }
  public void setRoll(float r) {
    roll = r;
  }
}

:)