Skip to content

Commit 7d77c13

Browse files
authored
Merge pull request #7561 from bojidar-bg/7342-screenToWorld
Add screenToWorld function mirroring worldToScreen
2 parents 23ce4fb + a3cb5db commit 7d77c13

File tree

7 files changed

+219
-35
lines changed

7 files changed

+219
-35
lines changed

src/accessibility/outputs.js

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -540,14 +540,9 @@ function outputs(p5, fn){
540540

541541
//gets position of shape in the canvas
542542
fn._getPos = function (x, y) {
543-
const untransformedPosition = new DOMPointReadOnly(x, y);
544-
const currentTransform = this._renderer.isP3D ?
545-
new DOMMatrix(this._renderer.calculateCombinedMatrix()) :
546-
this.drawingContext.getTransform();
547-
const { x: transformedX, y: transformedY } = untransformedPosition
548-
.matrixTransform(currentTransform);
549-
const canvasWidth = this.width * this._renderer._pixelDensity;
550-
const canvasHeight = this.height * this._renderer._pixelDensity;
543+
const { x: transformedX, y: transformedY } = this.worldToScreen(new this.Vector(x, y));
544+
const canvasWidth = this.width;
545+
const canvasHeight = this.height;
551546
if (transformedX < 0.4 * canvasWidth) {
552547
if (transformedY < 0.4 * canvasHeight) {
553548
return 'top left';

src/core/environment.js

Lines changed: 73 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1268,7 +1268,9 @@ function environment(p5, fn){
12681268
* of 3D objects.
12691269
*
12701270
* @method worldToScreen
1271-
* @param {p5.Vector} worldPosition The 3D coordinates in the world space.
1271+
* @param {Number|p5.Vector} x The x coordinate in world space. (Or a vector for all three coordinates.)
1272+
* @param {Number} y The y coordinate in world space.
1273+
* @param {Number} [z] The z coordinate in world space.
12721274
* @return {p5.Vector} A vector containing the 2D screen coordinates.
12731275
* @example
12741276
* <div>
@@ -1380,27 +1382,77 @@ function environment(p5, fn){
13801382
*
13811383
*/
13821384
fn.worldToScreen = function(worldPosition) {
1383-
const renderer = this._renderer;
1384-
if (renderer.drawingContext instanceof CanvasRenderingContext2D) {
1385-
// Handle 2D context
1386-
const transformMatrix = new DOMMatrix()
1387-
.scale(1 / renderer._pInst.pixelDensity())
1388-
.multiply(renderer.drawingContext.getTransform());
1389-
const screenCoordinates = transformMatrix.transformPoint(
1390-
new DOMPoint(worldPosition.x, worldPosition.y)
1391-
);
1392-
return new p5.Vector(screenCoordinates.x, screenCoordinates.y);
1393-
} else {
1394-
// Handle WebGL context (3D)
1395-
const modelViewMatrix = renderer.calculateCombinedMatrix();
1396-
const cameraCoordinates = modelViewMatrix.multiplyPoint(worldPosition);
1397-
const normalizedDeviceCoordinates =
1398-
renderer.states.uPMatrix.multiplyAndNormalizePoint(cameraCoordinates);
1399-
const screenX = (0.5 + 0.5 * normalizedDeviceCoordinates.x) * this.width;
1400-
const screenY = (0.5 - 0.5 * normalizedDeviceCoordinates.y) * this.height;
1401-
const screenZ = 0.5 + 0.5 * normalizedDeviceCoordinates.z;
1402-
return new Vector(screenX, screenY, screenZ);
1385+
if (typeof worldPosition === "number") {
1386+
// We got passed numbers, convert to vector
1387+
worldPosition = this.createVector(...arguments);
1388+
}
1389+
1390+
const matrix = this._renderer.getWorldToScreenMatrix();
1391+
const screenPosition = matrix.multiplyAndNormalizePoint(worldPosition);
1392+
return screenPosition;
1393+
};
1394+
/**
1395+
* Converts 2D screen coordinates to 3D world coordinates.
1396+
*
1397+
* This function takes a vector and converts its coordinates from coordinates
1398+
* on the screen to coordinates in the currently drawn object. This can be
1399+
* useful for determining the mouse position relative to a 2D or 3D object.
1400+
*
1401+
* If given, the Z component of the input coordinates is treated as "depth",
1402+
* or distance from the camera.
1403+
*
1404+
* @method screenToWorld
1405+
* @param {Number|p5.Vector} x The x coordinate in screen space. (Or a vector for all three coordinates.)
1406+
* @param {Number} y The y coordinate in screen space.
1407+
* @param {Number} [z] The z coordinate in screen space.
1408+
* @return {p5.Vector} A vector containing the 3D world space coordinates.
1409+
* @example
1410+
* <div>
1411+
* <code>
1412+
*
1413+
* function setup() {
1414+
* createCanvas(100, 100);
1415+
* describe('A rotating square with a line passing through the mouse drawn across it.');
1416+
* }
1417+
*
1418+
* function draw() {
1419+
* background(220);
1420+
*
1421+
* // Move to center and rotate
1422+
* translate(width/2, height/2);
1423+
* rotate(millis() / 1000);
1424+
* rect(-30, -30, 60);
1425+
*
1426+
* // Compute the location of the mouse in the coordinates of the square
1427+
* let localMouse = screenToWorld(createVector(mouseX, mouseY));
1428+
*
1429+
* // Draw a line parallel to the local Y axis, passing through the mouse
1430+
* line(localMouse.x, -30, localMouse.x, 30);
1431+
* }
1432+
*
1433+
* </code>
1434+
* </div>
1435+
*
1436+
*/
1437+
fn.screenToWorld = function(screenPosition) {
1438+
if (typeof screenPosition === "number") {
1439+
// We got passed numbers, convert to vector
1440+
screenPosition = this.createVector(...arguments);
1441+
}
1442+
1443+
const matrix = this._renderer.getWorldToScreenMatrix();
1444+
1445+
if (screenPosition.dimensions == 2) {
1446+
// Calculate a sensible Z value for the current camera projection that
1447+
// will result in 0 once converted to world coordinates
1448+
let z = matrix.mat4[14] / matrix.mat4[15];
1449+
screenPosition = this.createVector(screenPosition.x, screenPosition.y, z);
14031450
}
1451+
1452+
const matrixInverse = matrix.invert(matrix);
1453+
1454+
const worldPosition = matrixInverse.multiplyAndNormalizePoint(screenPosition);
1455+
return worldPosition;
14041456
};
14051457
}
14061458

src/core/p5.Renderer2D.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { Element } from '../dom/p5.Element';
77
import { MediaElement } from '../dom/p5.MediaElement';
88
import { RGBHDR } from '../color/creating_reading';
99
import FilterRenderer2D from '../image/filterRenderer2D';
10+
import { Matrix } from '../math/p5.Matrix';
1011
import { PrimitiveToPath2DConverter } from '../shape/custom_shapes';
1112

1213

@@ -999,6 +1000,13 @@ class Renderer2D extends Renderer {
9991000
this.drawingContext.transform(a, b, c, d, e, f);
10001001
}
10011002

1003+
getWorldToScreenMatrix() {
1004+
let domMatrix = new DOMMatrix()
1005+
.scale(1 / this._pixelDensity)
1006+
.multiply(this.drawingContext.getTransform());
1007+
return new Matrix(domMatrix.toFloat32Array());
1008+
}
1009+
10021010
resetMatrix() {
10031011
this.drawingContext.setTransform(1, 0, 0, 1, 0, 0);
10041012
this.drawingContext.scale(

src/math/p5.Vector.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,14 +34,14 @@ class Vector {
3434
// This is how it comes in with createVector()
3535
// This check if the first argument is a function
3636
constructor(...args) {
37-
let dimensions = args.length; // TODO: make default 3 if no arguments
3837
let values = args.map((arg) => arg || 0);
3938
if (typeof args[0] === "function") {
4039
this.isPInst = true;
4140
this._fromRadians = args[0];
4241
this._toRadians = args[1];
4342
values = args.slice(2).map((arg) => arg || 0);
4443
}
44+
let dimensions = values.length; // TODO: make default 3 if no arguments
4545
if (dimensions === 0) {
4646
this.dimensions = 2;
4747
this._values = [0, 0, 0];

src/webgl/p5.RendererGL.js

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1024,12 +1024,23 @@ class RendererGL extends Renderer {
10241024
this.clear(..._col._getRGBA());
10251025
}
10261026

1027-
// Combines the model and view matrices to get the uMVMatrix
1028-
// This method will be reusable wherever you need to update the combined matrix.
1029-
calculateCombinedMatrix() {
1027+
1028+
/**
1029+
* Get a matrix from world-space to screen-space
1030+
*/
1031+
getWorldToScreenMatrix() {
10301032
const modelMatrix = this.states.uModelMatrix;
10311033
const viewMatrix = this.states.uViewMatrix;
1032-
return modelMatrix.copy().mult(viewMatrix);
1034+
const projectionMatrix = this.states.uPMatrix;
1035+
const projectedToScreenMatrix = new Matrix(4);
1036+
projectedToScreenMatrix.scale(this.width, this.height, 1);
1037+
projectedToScreenMatrix.translate([0.5, 0.5, 0.5]);
1038+
projectedToScreenMatrix.scale(0.5, -0.5, 0.5);
1039+
1040+
const modelViewMatrix = modelMatrix.copy().mult(viewMatrix);
1041+
const modelViewProjectionMatrix = modelViewMatrix.mult(projectionMatrix);
1042+
const worldToScreenMatrix = modelViewProjectionMatrix.mult(projectedToScreenMatrix);
1043+
return worldToScreenMatrix;
10331044
}
10341045

10351046
//////////////////////////////////////////////
@@ -2204,7 +2215,7 @@ class RendererGL extends Renderer {
22042215
const modelMatrix = this.states.uModelMatrix;
22052216
const viewMatrix = this.states.uViewMatrix;
22062217
const projectionMatrix = this.states.uPMatrix;
2207-
const modelViewMatrix = this.calculateCombinedMatrix();
2218+
const modelViewMatrix = modelMatrix.copy().mult(viewMatrix);
22082219

22092220
shader.setUniform(
22102221
"uPerspective",

test/unit/core/environment.js

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,15 @@ suite('Environment', function() {
249249
assert.closeTo(screenPos.y, 50, 0.1);
250250
});
251251

252+
test('worldToScreen accepts numbers with translation in 2D', function() {
253+
myp5.push();
254+
myp5.translate(50, 50);
255+
let screenPos = myp5.worldToScreen(0, 0);
256+
myp5.pop();
257+
assert.closeTo(screenPos.x, 50, 0.1);
258+
assert.closeTo(screenPos.y, 50, 0.1);
259+
});
260+
252261
test('worldToScreen with rotation in 2D', function() {
253262
myp5.push();
254263
myp5.translate(50, 50);
@@ -259,6 +268,46 @@ suite('Environment', function() {
259268
assert.closeTo(screenPos.x, 50, 0.1);
260269
assert.closeTo(screenPos.y, 60, 0.1);
261270
});
271+
272+
test('screenToWorld for 2D context', function() {
273+
let screenPos = myp5.createVector(50, 50);
274+
let worldPos = myp5.screenToWorld(screenPos);
275+
assert.closeTo(worldPos.x, 50, 0.1);
276+
assert.closeTo(worldPos.y, 50, 0.1);
277+
});
278+
279+
test('screenToWorld accepts numbers with translation in 2D', function() {
280+
myp5.push();
281+
myp5.translate(50, 50);
282+
let worldPos = myp5.screenToWorld(50, 50);
283+
myp5.pop();
284+
assert.closeTo(worldPos.x, 0, 0.1);
285+
assert.closeTo(worldPos.y, 0, 0.1);
286+
});
287+
288+
test('screenToWorld with rotation in 2D', function() {
289+
myp5.push();
290+
myp5.translate(50, 50);
291+
myp5.rotate(myp5.PI / 2);
292+
let screenPos = myp5.createVector(50, 60);
293+
let worldPos = myp5.screenToWorld(screenPos);
294+
myp5.pop();
295+
assert.closeTo(worldPos.x, 10, 0.1);
296+
assert.closeTo(worldPos.y, 0, 0.1);
297+
});
298+
299+
test('screenToWorld is inverse of worldToScreen in 2D', function() {
300+
myp5.push();
301+
// Arbitrary rotation
302+
myp5.translate(10, 10);
303+
myp5.rotate(myp5.PI / 8);
304+
let origWorldPos = myp5.createVector(20, 20);
305+
let screenPos = myp5.worldToScreen(origWorldPos);
306+
let worldPos = myp5.screenToWorld(screenPos);
307+
myp5.pop();
308+
assert.closeTo(worldPos.x, origWorldPos.x, 0.1);
309+
assert.closeTo(worldPos.y, origWorldPos.y, 0.1);
310+
});
262311
});
263312

264313
suite('3D context test', function() {
@@ -273,6 +322,15 @@ suite('Environment', function() {
273322
assert.closeTo(screenPos.y, 50, 0.1);
274323
});
275324

325+
test('worldToScreen accepts numbers with translation in 3D', function() {
326+
myp5.push();
327+
myp5.translate(50, 50, 0);
328+
let screenPos = myp5.worldToScreen(0, 0, 0);
329+
myp5.pop();
330+
assert.closeTo(screenPos.x, 100, 0.1);
331+
assert.closeTo(screenPos.y, 100, 0.1);
332+
});
333+
276334
test('worldToScreen with rotation in 3D around Y-axis', function() {
277335
myp5.push();
278336
myp5.rotateY(myp5.PI / 2);
@@ -292,5 +350,57 @@ suite('Environment', function() {
292350
assert.closeTo(screenPos.x, 50, 0.1);
293351
assert.closeTo(screenPos.y, 60, 0.1);
294352
});
353+
354+
test('screenToWorld for 3D context', function() {
355+
let screenPos = myp5.createVector(50, 50);
356+
let worldPos = myp5.screenToWorld(screenPos);
357+
assert.closeTo(worldPos.x, 0, 0.1);
358+
assert.closeTo(worldPos.y, 0, 0.1);
359+
assert.closeTo(worldPos.z, 0, 0.1);
360+
});
361+
362+
test('screenToWorld accepts numbers with translation in 3D', function() {
363+
myp5.push();
364+
myp5.translate(50, 50);
365+
let worldPos = myp5.screenToWorld(100, 100);
366+
myp5.pop();
367+
assert.closeTo(worldPos.x, 0, 0.1);
368+
assert.closeTo(worldPos.y, 0, 0.1);
369+
});
370+
371+
test('screenToWorld with rotation in 3D around Y-axis', function() {
372+
myp5.push();
373+
myp5.rotateY(myp5.PI / 2);
374+
let screenPos = myp5.createVector(50, 50);
375+
let worldPos = myp5.screenToWorld(screenPos);
376+
myp5.pop();
377+
assert.closeTo(worldPos.y, 0, 0.1);
378+
});
379+
380+
test('screenToWorld with rotation in 3D around Z-axis', function() {
381+
myp5.push();
382+
myp5.rotateZ(myp5.PI / 2);
383+
let screenPos = myp5.createVector(50, 60);
384+
let worldPos = myp5.screenToWorld(screenPos);
385+
myp5.pop();
386+
assert.closeTo(worldPos.x, 10, 0.1);
387+
assert.closeTo(worldPos.y, 0, 0.1);
388+
});
389+
390+
test('screenToWorld is inverse of worldToScreen in 3D', function() {
391+
myp5.push();
392+
// Arbitrary rotation
393+
myp5.translate(10, 10);
394+
myp5.rotateX(myp5.PI / 3);
395+
myp5.rotateY(myp5.PI / 4);
396+
myp5.rotateZ(myp5.PI / 8);
397+
let origWorldPos = myp5.createVector(20, 20, 0);
398+
let screenPos = myp5.worldToScreen(origWorldPos);
399+
let worldPos = myp5.screenToWorld(screenPos);
400+
myp5.pop();
401+
assert.closeTo(worldPos.x, origWorldPos.x, 0.1);
402+
assert.closeTo(worldPos.y, origWorldPos.y, 0.1);
403+
assert.closeTo(worldPos.z, origWorldPos.z, 0.1);
404+
});
295405
});
296406
});

test/unit/math/p5.Vector.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,10 @@ suite("p5.Vector", function () {
5252
assert.equal(v.y, 0);
5353
assert.equal(v.z, 0);
5454
});
55+
56+
test("should have dimensions initialized to 2", function () {
57+
assert.equal(v.dimensions, 2);
58+
});
5559
});
5660

5761
suite.todo("p5.prototype.createVector(1, 2, 3)", function () {
@@ -64,6 +68,10 @@ suite("p5.Vector", function () {
6468
assert.equal(v.y, 2);
6569
assert.equal(v.z, 3);
6670
});
71+
72+
test("should have dimensions initialized to 3", function () {
73+
assert.equal(v.dimensions, 3);
74+
});
6775
});
6876

6977
suite("new p5.Vector()", function () {

0 commit comments

Comments
 (0)