Skip to content

Commit fc3cac6

Browse files
Add screenToWorld function mirroring worldToScreen
Co-authored-by: Garima <[email protected]>
1 parent 06107f8 commit fc3cac6

File tree

2 files changed

+169
-0
lines changed

2 files changed

+169
-0
lines changed

src/core/environment.js

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1406,6 +1406,102 @@ function environment(p5, fn){
14061406
return new Vector(screenX, screenY, screenZ);
14071407
}
14081408
};
1409+
/**
1410+
* Converts 2D screen coordinates to 3D world coordinates.
1411+
*
1412+
* This function takes a vector and converts its coordinates
1413+
* from the world space to screen space. This can be useful for determining
1414+
* the mouse position relative to a 2D or 3D object.
1415+
*
1416+
* The Z component of the screen coordinates is treated as "depth", or
1417+
* distance from the camera, when converting to 3D world space.
1418+
*
1419+
* @method screenToWorld
1420+
* @param {p5.Vector} screenPosition The coordinates in screen space.
1421+
* @return {p5.Vector} A vector containing the 3D world space coordinates.
1422+
* @example
1423+
* <div>
1424+
* <code>
1425+
*
1426+
* function setup() {
1427+
* createCanvas(100, 100);
1428+
* describe('A rotating square with a line passing through the mouse drawn across it.');
1429+
* }
1430+
*
1431+
* function draw() {
1432+
* background(220);
1433+
*
1434+
* // Move to center and rotate
1435+
* translate(width/2, height/2);
1436+
* rotate(millis() / 1000);
1437+
* rect(-30, -30, 60);
1438+
*
1439+
* // Compute the location of the mouse in the coordinates of the square
1440+
* let localMouse = screenToWorld(createVector(mouseX, mouseY));
1441+
*
1442+
* // Draw a line parallel to the local Y axis, passing through the mouse
1443+
* line(localMouse.x, -30, localMouse.x, 30);
1444+
* }
1445+
*
1446+
* </code>
1447+
* </div>
1448+
* @example
1449+
* <div>
1450+
* <code>
1451+
*
1452+
* function setup() {
1453+
* createCanvas(100, 100, WEBGL);
1454+
* describe('A rotating square with a line passing through the mouse drawn across it.');
1455+
* }
1456+
*
1457+
* function draw() {
1458+
* background(220);
1459+
*
1460+
* // Animate rotation
1461+
* rotateZ(millis() / 1000);
1462+
* rect(-30, -30, 60);
1463+
*
1464+
* // Transform the 0, 0 point to get the depth of the square
1465+
* let center = worldToScreen(createVector(0, 0))
1466+
*
1467+
* // Compute the location of the mouse in the coordinates of the square,
1468+
* // using the depth calculated earlier
1469+
* let localMouse = screenToWorld(createVector(mouseX, mouseY, center.z));
1470+
*
1471+
* // Draw a line parallel to the local Y axis, passing through the mouse
1472+
* line(localMouse.x, -30, localMouse.x, 30);
1473+
* }
1474+
*
1475+
* </code>
1476+
* </div>
1477+
*
1478+
*/
1479+
fn.screenToWorld = function(screenPosition) {
1480+
const renderer = this._renderer;
1481+
if (renderer.drawingContext instanceof CanvasRenderingContext2D) {
1482+
// Handle 2D context
1483+
const inverseTransformMatrix = new DOMMatrix()
1484+
.multiply(renderer.drawingContext.getTransform().inverse())
1485+
.scale(renderer._pInst.pixelDensity());
1486+
const screenCoordinates = inverseTransformMatrix.transformPoint(
1487+
new DOMPoint(screenPosition.x, screenPosition.y)
1488+
);
1489+
return new p5.Vector(screenCoordinates.x, screenCoordinates.y);
1490+
} else {
1491+
const normalizedDeviceCoordinates = new p5.Vector(
1492+
screenPosition.x / this.width * 2 - 1,
1493+
screenPosition.y / this.height * -2 + 1,
1494+
screenPosition.z * 2 - 1,
1495+
);
1496+
let uPMatrixInverse = renderer.states.uPMatrix.copy()
1497+
uPMatrixInverse = uPMatrixInverse.invert(uPMatrixInverse);
1498+
let modelViewMatrixInverse = renderer.calculateCombinedMatrix();
1499+
modelViewMatrixInverse = modelViewMatrixInverse.invert(modelViewMatrixInverse);
1500+
const cameraCoordinates = uPMatrixInverse.multiplyAndNormalizePoint(normalizedDeviceCoordinates);
1501+
const worldPosition = modelViewMatrixInverse.multiplyPoint(cameraCoordinates);
1502+
return worldPosition;
1503+
}
1504+
};
14091505
}
14101506

14111507
export default environment;

test/unit/core/environment.js

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,37 @@ suite('Environment', function() {
259259
assert.closeTo(screenPos.x, 50, 0.1);
260260
assert.closeTo(screenPos.y, 60, 0.1);
261261
});
262+
263+
test('screenToWorld for 2D context', function() {
264+
let screenPos = myp5.createVector(50, 50);
265+
let worldPos = myp5.screenToWorld(screenPos);
266+
assert.closeTo(worldPos.x, 50, 0.1);
267+
assert.closeTo(worldPos.y, 50, 0.1);
268+
});
269+
270+
test('screenToWorld with rotation in 2D', function() {
271+
myp5.push();
272+
myp5.translate(50, 50);
273+
myp5.rotate(myp5.PI / 2);
274+
let screenPos = myp5.createVector(50, 60);
275+
let worldPos = myp5.screenToWorld(screenPos);
276+
myp5.pop();
277+
assert.closeTo(worldPos.x, 10, 0.1);
278+
assert.closeTo(worldPos.y, 0, 0.1);
279+
});
280+
281+
test('screenToWorld is inverse of worldToScreen in 2D', function() {
282+
myp5.push();
283+
// Arbitrary rotation
284+
myp5.translate(10, 10);
285+
myp5.rotate(myp5.PI / 8);
286+
let origWorldPos = myp5.createVector(20, 20);
287+
let screenPos = myp5.worldToScreen(origWorldPos);
288+
let worldPos = myp5.screenToWorld(screenPos);
289+
myp5.pop();
290+
assert.closeTo(worldPos.x, origWorldPos.x, 0.1);
291+
assert.closeTo(worldPos.y, origWorldPos.y, 0.1);
292+
});
262293
});
263294

264295
suite('3D context test', function() {
@@ -292,5 +323,47 @@ suite('Environment', function() {
292323
assert.closeTo(screenPos.x, 50, 0.1);
293324
assert.closeTo(screenPos.y, 60, 0.1);
294325
});
326+
327+
test('screenToWorld for 3D context', function() {
328+
let screenPos = myp5.createVector(50, 50);
329+
let worldPos = myp5.screenToWorld(screenPos);
330+
assert.closeTo(worldPos.x, 0, 0.1);
331+
assert.closeTo(worldPos.y, 0, 0.1);
332+
});
333+
334+
test('screenToWorld with rotation in 3D around Y-axis', function() {
335+
myp5.push();
336+
myp5.rotateY(myp5.PI / 2);
337+
let screenPos = myp5.createVector(50, 50, 10/11);
338+
let worldPos = myp5.screenToWorld(screenPos);
339+
myp5.pop();
340+
assert.closeTo(worldPos.y, 0, 0.1);
341+
});
342+
343+
test('screenToWorld with rotation in 3D around Z-axis', function() {
344+
myp5.push();
345+
myp5.rotateZ(myp5.PI / 2);
346+
let screenPos = myp5.createVector(50, 60, 10/11);
347+
let worldPos = myp5.screenToWorld(screenPos);
348+
myp5.pop();
349+
assert.closeTo(worldPos.x, 10, 0.1);
350+
assert.closeTo(worldPos.y, 0, 0.1);
351+
});
352+
353+
test('screenToWorld is inverse of worldToScreen in 3D', function() {
354+
myp5.push();
355+
// Arbitrary rotation
356+
myp5.translate(10, 10);
357+
myp5.rotateX(myp5.PI / 3);
358+
myp5.rotateY(myp5.PI / 4);
359+
myp5.rotateZ(myp5.PI / 8);
360+
let origWorldPos = myp5.createVector(20, 20, 0);
361+
let screenPos = myp5.worldToScreen(origWorldPos);
362+
let worldPos = myp5.screenToWorld(screenPos);
363+
myp5.pop();
364+
assert.closeTo(worldPos.x, origWorldPos.x, 0.1);
365+
assert.closeTo(worldPos.y, origWorldPos.y, 0.1);
366+
assert.closeTo(worldPos.z, origWorldPos.z, 0.1);
367+
});
295368
});
296369
});

0 commit comments

Comments
 (0)