I'm facing a troublesome problem while trying to create a game engine in threeJS.
It is a math problem, but also a programming problem.
I've implemented a velocity based movement system for the player's avatar - I've used a tank in this example.
Currently, when the player hits a wall, regardless of the angle, the tank invariably stops dead.
However, I want it to be the case that the tank's velocity changes, having been coerced to follow the angle of the wall, and also reduced by a magnitude that is related to that angle.
For example, in FIG A, upon hitting the wall, the Tank continues to try and move forwards, but it's velocity is altered so that it now moves forwards, and sideways, at a reduced rate.
In FIG B, the tank hits the wall dead-on, and its overall velocity reaches 0.
In FIG C, the tank glances off the wall, and its overall velocity is only reduced by a small amount.
I've realised that I need to somehow combine the Tank's velocity vector with the wall's normal vector, to produce the adjusted vector, but I am struggling with how to represent this mathematically / programmatically.
I've tried using: tank.velocity.multiply(wallFaceNormal); (both tank.velocity and wallFaceNormal are Vector3 objects.) but this only seems to work as intended when the wall is either at angles of 0, 90, 180 or 270.
since a tank will not jump or fly, you should be fine with just a 2D-System for your calculation?
i found a link describing the physics of car hitting a solid brick wall.
http://colgatephys111.blogspot.com/2017/12/guardrail-lessens-force-of-impact.html
hope thats gonna help you a bit!
edit:
so, out of curiosity, i asked an theoretical physicist over the phone about your issue.
you got 2 seperate problems to solve:
1. P1 what is the velocity v' while hitting the wall?
2. P2 what is the new angle of the vehicel?
P2 should be fairly easy, considering your tank is adapting the angle of the wall you only need to calculate in which direction the wall is "pointing".
P1 in physics, we would talk about the reduced force and not the velocity, but given a constant limit to the force F1 (eg. your engine) resulting in a constant maxspeed,
and with a given force the wall has on the vehicel F2
v = F1
v' = F1'
F1' = F1 - F2
i think
https://www.thoughtco.com/what-is-the-physics-of-a-car-collision-2698920
explains what to do
Some code provided by a Physicist, which partly worked when I converted it to Javascript and applied it to the program:
Vector3 wallNormal = new Vector3(-0.5, 0.0, 0.5);
Vector3 incomingVelocity = new Vector3(0.0, 0.0, -1.0);
double magnitudeProduct = wallNormal.Length() * incomingVelocity.Length();
double angleBetweenVelocityAndWall = ((-incomingVelocity).Dot(wallNormal)) / (magnitudeProduct);
double newVelocityMagnitude = incomingVelocity.Length() * Math.Sin(angleBetweenVelocityAndWall);
Vector3 upVector =incomingVelocity.Cross(wallNormal);
Vector3 newDirection = wallNormal.Cross(upVector);
Vector3 newVelocity = newDirection.Normalise() * newVelocityMagnitude;
I've done some work on this problem and produced a mini game "framework" that includes an environment collision and movement attenuation utility.
I've written an article that explains how it works, which can be found here. http://www.socket-two.com/main/resource/hdoc-tutorial
But for the sake of the integrity of the thread, here's an adaptation of the portion that describes one of the approaches that can be used to attenuate motion in a ThreeJS simulation:
...
Crucially, my interest has not been to create games that involve large amount of physics, but just to create games where:
A player cannot walk through walls
A player cannot fall through floors
I've made a handful of attempts at implementing a system that would achieve this behaviour, but none of them have really worked satisfactorily. Until now.
In terms of how the ECS fits into the app architecture, it is a utility class. This is its API shape:
class Planeclamp {
constructor({ floors /*Mesh[]*/, walls /*Mesh[]*/ })
getSafePosition(startingPositionIn /*Vector3*/, intendedPositionIn /*Vector3*/) // Returns safePosition, which is a Vector3
}
As you can see, its a class that accepts two arrays of meshes in its constructor: Meshes that should be treated as floors, and meshes that should be treated as walls. Now of course in reality, there is no clear distinction between a steep floor and a shallow-angled wall, but for the purposes of the simulation, the distinction has a very reasonable integrity, and will simplify the environment collision system logic greatly.
Once you've constructed an instance of the Planeclamp class, you can then invoke it's getSafePosition method, to transform a starting position and an intended position into an attenuated position. Being the discerning reader that you are, you will have deduced that the attenuated position is the intended position, having been changed a bit if any collisions have been detected by the utility.
This is how it can be used in the game loop, to ensure a player does not pass through walls or floors:
const planeclamp = new Planeclamp({
floors: [someFloorMesh, someOtherMesh],
walls: [houseMesh, perimeterMesh, truckMesh],
});
const player = new Player();
console.log(player.cage); // Object3D
let playerPreviousPosition = player.cage.position; // Vector3
function gameLoop(delta) {
const playerIntendedPosition = new Three.Vector3(
playerPreviousPosition.x,
playerPreviousPosition.y + (10 * delta), // i.e. Gravity
playerPreviousPosition.z + (1 * delta), // i.e. Walking forwards
);
let {
safePosition, // Vector3
grounded, // Boolean
groundMaterial, // String
} = planeclamp.getSafePosition(playerPreviousPosition, playerIntendedPosition);
player.cage.position.copy(safePosition);
playerPreviousPosition = player.cage.position; // Vector3
}
And thats about it! If you would like to use this utility, you can find it in the repository. But if you would like to know more about the logic behind its workings, read on.
The Planeclamp.getSafePosition method works out a safe position in two stages. Firstly, it uses a vertical raycaster to take a look at what is underneath the player, to then see if it should stop the player from moving downwards any further. Secondly, it uses horizontal raycasters to see if it should stop the player from moving horizontally. Lets look at the vertical constraint procedure first - this is the more simple of the two steps.
// Before we do anything, create a variable called "gated".
// This will contain the safe new position that we will return at the end of
// the function. When creating it, we let it default to the
// intended position. If collisions are detected throughout the lifecycle
// of this function, these values will be overwritten.
let gated = {
x: intendedPosition.x,
y: intendedPosition.y,
z: intendedPosition.z,
};
// Define the point in 3D space where we will shoot a ray from.
// For those who haven't used raycasters before, a ray is just a line with a direction.
// We use the player's intended position as the origin of the ray, but we
// augment this by moving the origin up a little bit (backStepVert) to prevent tunneling.
const start = intendedPosition.clone().sub(new Three.Vector3(
0,
(backStepVert * -1) - (heightOffset / 2),
0)
);
// Now, define the direction of the ray, in the form of a vector.
// By giving the vector X and Z values of 0, and a Y value of -1,
// the ray shoots directly downwards.
const direction = new Three.Vector3(0, -1, 0).normalize();
// We now set the origin and direction of a raycaster that we instantiated
// in the class constructor method.
this.raycasters.vert.set(start, direction);
// Now, we use the `intersectObjects` method of the ray.
// This will return to us an array, filled with information about each
// thing that the ray collided with.
const dirCollisions = this.raycasters.vert.intersectObjects(this.floors, false);
// Initialise a distanceToGround, a grounded variable, and a groundMaterial variable.
let distanceToGround = null;
let grounded = false;
let groundMaterial = null;
// If the dirCollisions array has at least one item in it, the
// ray passed through one of our floor meshes.
if (dirCollisions.length) {
// ThreeJS returns the nearest intersection first in the collision
// results array. As we are only interested in the nearest collision,
// we pluck it out, and ignore the rest.
const collision = dirCollisions[0];
// Now, we work out the distance between where the players feet
// would be if the players intended position became the players
// actual position, and the collided object.
distanceToGround = collision.distance - backStepVert - heightOffset;
// If the distance is less than 0, then the player will pass through
// the groud if their intended position is allowed to become
// their actual position.
if (distanceToGround < 0) {
// We dont want that to hapen, so lets set the safe gated.y coordinate
// to the y coordinate of the point in space at which the collision
// happened. In other words, exactly where the ground is.
gated.y = intendedPosition.y - distanceToGround;
// Make a note that the player is now grounded.
// We return this at the end of the function, along with
// the safe position.
grounded = true;
// If the collided object also has a groundMaterial set inside
// its userData (the place that threeJS lets us attach arbitrary
// info to our objects), also set the groundMaterial. This is
// also returned at the end of the function alongside the grounded
// variable.
if (collision.object.userData.groundMaterial) {
groundMaterial = collision.object.userData.groundMaterial;
}
}
}
And thats it for vertical environment constraints. Simples!
The horizontal environment constraint system is a bit more complex. But in its essence, what it does is:
Work out the horizontal direction the player is travelling in. In olde worlde terms, this can be thought of as North, South, SouthEast, SouthSouthWest etc, but in ThreeJS it is represented by a Vector.
Cast a ray in the direction that the player is travelling in.
Use the ray to find out if allowing the players intended position would cause the player to pass through any of the wall meshes.
And it is at this point that the horizontal ECS becomes more complex than the vertical ECS. With the vertical ECS, if a collision happens, we can just set the players Y position to the Y position of the point at which the collision happened - effectively halting the players Y movement. However, it we did this for horizontal movement, it would make for a very frustrating game experience.
If the player was running head on into a wall, and was stopped dead in their tracks, this would be fine. But if the player moved into the wall at a very shallow angle, and merely grazed it, it would appear that they had "gotten stuck" on the wall, and would find themselves having to reverse away from it, and take care not to touch it again.
What we actually want to happen, is have the player's horizontal velocity attenuated, so that they move along the wall. Therefore, the horizontal ECS proceeds as follows:
Obtain the normal of the surface that was collided with. (For our purposes, a normal can be described as the direction that the wall is facing)
Inspect the difference between the wall normal direction, and the player's movement direction.
Use the difference to work out a safe position, which is the point in space that the collision happened, incremented by a vector that is horizontally perpendicular to the wall normal, multiplied by the cross product of the players input direction and the wall normal.
...
Here is the final utility class, in full:
import * as Three from '../../../vendor/three/three.module.js';
class Planeclamp {
constructor({
scene,
floors = [],
walls = [],
drawRays = true,
} = {}) {
this.drawRays = drawRays;
this.floors = [];
this.walls = [];
this.scene = scene;
this.objects = [];
// Init collidable mesh lists
this.addFloors(floors);
this.addWalls(walls);
// Create rays
this.raycasters = {
vert: new Three.Raycaster(),
horzLeft: new Three.Raycaster(),
horzRight: new Three.Raycaster(),
correction: new Three.Raycaster(),
};
}
setDrawRays(draw) {
this.drawRays = draw;
}
addFloor(floor) {
this.floors.push(floor);
}
removeFloor(floor) {
this.floors = this.floors.filter(thisFloor => thisFloor !== floor);
}
addFloors(floors) {
floors.forEach(floor => this.addFloor(floor));
}
resetFloors() {
this.floors = [];
}
addWall(wall) {
this.walls.push(wall);
}
removeWall(wall) {
this.walls = this.walls.filter(thisWall => thisWall !== wall);
}
addWalls(walls) {
walls.forEach(wall => this.addWall(wall));
}
resetWalls() {
this.walls = [];
}
getSafePosition(startingPositionIn, intendedPositionIn, {
collisionPadding = .5,
heightOffset = 0,
} = {}) {
// ------------------ Setup -------------------
// Parse args
const startingPosition = startingPositionIn.clone();
const intendedPosition = intendedPositionIn.clone();
let grounded = false;
let groundMaterial = null;
// Augmenters
const backStepVert = 50;
const backStepHorz = 5;
const backStepCorrection = 5;
// Prepare output
let gated = {
x: intendedPosition.x,
y: intendedPosition.y,
z: intendedPosition.z,
};
// Clean up previous debug visuals
this.objects.map(object => this.scene.remove(object));
this.objects = [];
// ------------------ Vertical position gating -------------------
// Adjust vertical position in gated.y.
// Store grounded status in grounded.
const start = intendedPosition.clone().sub(new Three.Vector3(
0,
(backStepVert * -1) - (heightOffset / 2),
0)
);
const direction = new Three.Vector3(0, -1, 0).normalize();
this.raycasters.vert.set(start, direction);
const dirCollisions = this.raycasters.vert.intersectObjects(this.floors, false);
if (this.drawRays) {
const arrowColour = dirCollisions.length ? 0xff0000 : 0x0000ff;
const arrow = new Three.ArrowHelper(this.raycasters.vert.ray.direction, this.raycasters.vert.ray.origin, 300, arrowColour);
this.objects.push(arrow);
}
let distanceToGround = null;
if (dirCollisions.length) {
const collision = dirCollisions[0];
distanceToGround = collision.distance - backStepVert - heightOffset;
if (distanceToGround < 0) {
gated.y = intendedPosition.y - distanceToGround;
grounded = true;
if (collision.object.userData.groundMaterial) {
groundMaterial = collision.object.userData.groundMaterial;
}
}
}
// ------------------ Horizontal position gating -------------------
const horizontalOutputPosition = (() => {
// Init output position
const outputPosition = new Three.Vector3(intendedPosition.x, 0, intendedPosition.z);
// Store normalised input vector
const startingPos = startingPosition.clone();
const intendedPos = intendedPosition.clone();
startingPos.y = startingPositionIn.y + .5;
intendedPos.y = startingPositionIn.y + .5;
let inputVector = intendedPos.clone().sub(startingPos).normalize();
// Work out distances
const startingIntendedDist = startingPos.distanceTo(intendedPos);
const inputSpeed = startingIntendedDist;
// Define function for moving ray left and right
function adj(position, offset) {
const rayAdjuster = inputVector
.clone()
.applyAxisAngle(new Three.Vector3(0, 1, 0), Math.PI / 2)
.multiplyScalar(.5)
.multiplyScalar(offset);
return position.clone().add(rayAdjuster);
}
// Work out intersections and collision
let collisions = {
left: {
collision: null
},
right: {
collision: null
}
};
Object.keys(collisions).forEach(side => {
const rayOffset = side === 'left' ? -1 : 1;
const rayStart = adj(startingPos.clone().sub(inputVector.clone().multiplyScalar(2)), rayOffset);
const startingPosSide = adj(startingPos, rayOffset);
const intendedPosSide = adj(intendedPos, rayOffset);
const startingIntendedDistSide = startingPosSide.distanceTo(intendedPosSide);
const rayKey = 'horz' + _.startCase(side);
this.raycasters[rayKey].set(rayStart, inputVector);
const intersections = this.raycasters[rayKey].intersectObjects(this.walls, true);
for (let i = 0; i < intersections.length; i++) {
if (collisions[side].collision) break;
const thisIntersection = intersections[i];
const startingCollisionDist = startingPosSide.distanceTo(thisIntersection.point);
if (startingCollisionDist - collisionPadding <= startingIntendedDistSide) {
collisions[side].collision = thisIntersection;
collisions[side].offset = rayOffset;
}
}
if (inputSpeed && this.drawRays) {
this.objects.push(new Three.ArrowHelper(this.raycasters[rayKey].ray.direction, this.raycasters[rayKey].ray.origin, 300, 0x0000ff));
}
});
const [ leftCollision, rightCollision ] = [ collisions.left.collision, collisions.right.collision ];
const collisionData = (leftCollision?.distance || Infinity) < (rightCollision?.distance || Infinity) ? collisions.left : collisions.right;
if (collisionData.collision) {
// Var shorthands
const collision = collisionData.collision;
const normalVector = collision.face.normal.clone();
normalVector.transformDirection(collision.object.matrixWorld);
normalVector.normalize();
// Give output a baseline position that is the same as the collision position
let paddedCollision = collision.point.clone().sub(inputVector.clone().multiplyScalar(collisionPadding));
paddedCollision = adj(paddedCollision, collisionData.offset * -1);
outputPosition.x = paddedCollision.x;
outputPosition.z = paddedCollision.z;
if (leftCollision && rightCollision && leftCollision.face !== rightCollision.face) {
return startingPos;
}
// Work out difference between input vector and output / normal vector
const iCAngleCross = inputVector.clone().cross(normalVector).y; // -1 to 1
// Work out output vector
const outputVector = (() => {
const ivn = inputVector.clone().add(normalVector);
const xMultiplier = ivn.x > 0 ? 1 : -1;
const zMultiplier = ivn.z > 0 ? 1 : -1;
return new Three.Vector3(
Math.abs(normalVector.z) * xMultiplier,
0,
Math.abs(normalVector.x) * zMultiplier,
).normalize();
})();
if (inputSpeed && this.drawRays) {
this.objects.push(new Three.ArrowHelper(normalVector, startingPos, 300, 0xff0000));
}
// Work out output speed
const outputSpeed = inputSpeed * Math.abs(iCAngleCross) * 0.8;
// Increment output position with output vector X output speed
outputPosition.add(outputVector.clone().multiplyScalar(outputSpeed));
}
// ------------------ Done -------------------
return outputPosition;
})();
gated.x = horizontalOutputPosition.x;
gated.z = horizontalOutputPosition.z;
// ------------------ Culmination -------------------
// Add debug visuals
this.objects.map(object => this.scene.add(object));
// Return gated position
const safePosition = new Three.Vector3(gated.x, gated.y, gated.z);
return { safePosition, grounded, groundMaterial };
}
}
export default Planeclamp;
Lets say that on a 2d grid of 20x20, you have your character at position (5,5).
He is able to walk up to 4 tiles in his move. However, there may be obstacles blocking your path such as a wall.
Is there any efficient / easy way for calculating exactly which tiles he would be able to walk to without checking every single possible move ( e.g. move up 0 and right 0 then move up 0 and right 1 e.t.c )?
At the moment I'm calculating the places that you can walk through with this horrific thing:
int playerx = GridPane.getRowIndex(button);
int playery = GridPane.getColumnIndex(button);
int position = playery*8+playerx;
for (int i = 0; i < 5; i++)
{
for (int j = i-4; j < 5-i; j++)
{
try
{
int expectedCollumn = playerx+j;
int actualCollumn = ((position+i+j*8)-((position+i+j*8)%8))/8;
if(expectedCollumn==actualCollumn)
{
Button temp = (Button)gridPane.getChildren()
.get(position+i+j*8);
if (!temp.getText().equals("W") &&
!temp.getText().equals("P"))
{
temp.setText("T");
}
}
actualCollumn = ((position-i+j*8)-((position-i+j*8)%8))/8;
if(expectedCollumn==actualCollumn)
{
Button temp2 = (Button)
gridPane.getChildren().get(position-i+j*8);
if (!temp2.getText().equals("W") &&
!temp2.getText().equals("P"))
{
temp2.setText("T");
}
}
}
}
}
However, its showing as if you are able to walk to the otherside of the wall and I'm not sure how I would go about fixing this.
Many thanks in advance.
For path finding, you should figure out how this works:
https://en.wikipedia.org/wiki/Dijkstra%27s_algorithm
and then move on to A* or something more efficient.
Thanks to everyone that answered but the solution was simple
incase somehow someone finds this post and is interested it was a simple recursive call
void getReachableTiles(Tile current, Int stamina, List<Tile> visited, List<Tile> reachable) {
if (stamina <= 0) return;
List<Tile> neighbours = new List<>(current + up, current + left, ..)
for (Tile t in neighbours) {
if (!visited.contains(t)) {
visited.append(t);
if (!t.isWall()) {
reachable.append(t);
getReachableTiles(t, stamina - 1, visited, reachable);
}
}
}
}
i am trying to create a kind of metaball, nice curves between two circles.
Something like the image, the lines are drawn straight but can also be more curved. I need them as a vector in Processing. Does anyone can help me?
thanks in advance!
Example in paperjs:
http://paperjs.org/examples/meta-balls/
image:
http://www.smeulders.biz/tmp/metaballs.png
void setup() {
size(500,500);
ellipse(100, 250, 100, 100);
ellipse(350, 250, 200, 200);
}
void draw() {}
With a bit of math (to workout distance between circles) and a bit of pixel manipulation to set pixel colours based on these calculated distances, you can render 2D metaballs and there plenty of examples
For fun however I decided to take a stab at making a very hacky version of the example you shared by simply rendering ellipses into an image, then filtering the image at the end:
PGraphics pg;//a separate layer to render into
int dilateAmt = 3;
PImage grid;//pixels of the grid alone, minus the 'cursor'
void setup(){
size(400,400);
//create a new layer
pg = createGraphics(width,height);
pg.beginDraw();
//draw a di-grid inside
pg.background(255);
pg.noStroke();pg.fill(0);
for(int y = 0 ; y < 5; y++)
for(int x = 0 ; x < 5; x++)
pg.ellipse((y%2==0?40:0)+(x * 80),40+(y * 80), 40, 40);
pg.endDraw();
//grab a snapshot for later re-use
grid = pg.get();
}
void draw(){
pg.beginDraw();
//draw the cached grid (no need to loop and re-render circles)
pg.image(grid,0,0);
//and the cursor into the layer
pg.ellipse(mouseX,mouseY,60,60);
pg.endDraw();
//since PGraphics extends PImage, you can filter, so we dilate
for(int i = 0; i < dilateAmt; i++) pg.filter(DILATE);
//finally render the result
image(pg,0,0);
}
void keyPressed(){
if(keyCode == UP) dilateAmt++;
if(keyCode == DOWN) dilateAmt--;
if(dilateAmt < 1) dilateAmt = 1;
println(dilateAmt);
}
Note that the end result is raster, not vector.
If you want to achieve the exact effect you will need to port your example from JavaScript to Java. The source code is available.
If you like Processing the above example you could use plain javascript using p5.js. You'll find most of the familiar functions from Processing, but also directly use the paper.js library.
Trying to get an object to fire towards and through a given position. Like a bullet firing towards the mouse location, I don't want it to stop on the mouse (which is what is happening now).
Below is what I have so far, is there a function like lerp that I could use?
var speed:float;
var startPoint:Vector3;
var startTime:float;
var clickedPosition:Vector3;
function Start()
{
startPoint = transform.position;
startTime = Time.time;
clickedPosition = Vector3(Input.mousePosition.x, Input.mousePosition.y, 0);
clickedPosition = Camera.main.ScreenToWorldPoint(clickedPosition);
}
function Update ()
{
transform.position = Vector3.Lerp(startPoint, clickedPosition, (Time.time-startTime));
}
I would suggest using a rigidbody component and then applying a force in the direction (while disabling gravitiy i guess).
The way you have it now you should probably get it to work with
var speed : float;
function Start()
{
speed = 1000.0f; // experiment with this, might be way too fast;
...
}
function Update()
{
transform.position += (clickedPosition - startPoint) * speed * Time.deltaTime;
}
(clickedPosition - startPoint) should give you the direction in which you want to move the object, Time.deltaTime gives you the milliseconds since the last call of the Update function (you want this in here so that the object moves the same speed at different framerates) and speed is just a constant to adjust the velocity.
var speed:float;
var startPoint:Vector3;
var startTime:float;
var clickedPosition:Vector3;
function Start()
{
startPoint = transform.position;
}
function Update ()
{
if(Input.GetKeyDown(KeyCode.Mouse0))
{
startTime = Time.time;
clickedPosition = Vector3(Input.mousePosition.x, Input.mousePosition.y, 0);
clickedPosition = Camera.main.ScreenToWorldPoint(clickedPosition);
}
transform.position = Vector3.Lerp(startPoint, clickedPosition, (Time.time-startTime));
}
This is pretty simple. You can use the Vector3.Lerp function to achieve this. Use raycasting to get the mouse click position or the touch position. Then use the initial and the final position in the lerp function. The initial position being the position that the gameobject is at now and the final position being the click / touch position.
You can find the article on the same here
Move to Touch / Click Position - The Game Contriver
I am getting started with JavaFX and basically what I am trying to implement is a Color Picker.
At first, I thought of having a rectangle with a LinearGradient that goes through all primary/secondary colors.
Looks like what I want, but the problem is that I can not get the RGB values at a given coordinate(x,y) in this Node.
I know you can do it through the fill property of any Shape IF it is a Color.
But Is there anyway to get the RGB values of anything inside a LinearGradient/Paint ?
Does this ColorPicker JavaFX example help?
[...]
function colorAtLocation(x:Integer, y:Integer) : Color {
var bimg = iv.image.bufferedImage;
if (x < 0 or x >= bimg.getWidth() or y < 0 or y >= bimg.getHeight()) {
return null;
}
var rgb = bimg.getRGB(x, y);
var r = Bits.bitAnd(Bits.shiftRight(rgb, 16), 0xff);
var g = Bits.bitAnd(Bits.shiftRight(rgb, 8), 0xff);
var b = Bits.bitAnd(Bits.shiftRight(rgb, 0), 0xff);
Color.rgb(r, g, b)
}
function updateSelectedColor(e:MouseEvent) {
var rgb = colorAtLocation(e.x, e.y);
if (rgb != null) {
picker.selectedColor = rgb;
}
}
[...]
The ColorPicker JavaFX example starts with a png image that is loaded to an image that then populates the ImageView.
The question starts with a JavaFX Rectangle containing LinearGradient.
To get the rectangle contents into a buffered image, one can use the java.awt.Robot:
var rectangle = new java.awt.Rectangle(x,y,width,height);
var robot = new java.awt.Robot();
var bufferedImage = robot.createScreenCapture(rectangle);
where rectangle would be describe the coordinates of the JavaFX Rectangle containing the bits of interest.
The robot.createScreenCapture call has the gotcha that to do the screen capture, the screen has to be visible. There should be a better of way to populate the buffered image but I've not yet encountered it.