diff --git a/src/core/environment.js b/src/core/environment.js
index 2e4b711632..f129ff71b2 100644
--- a/src/core/environment.js
+++ b/src/core/environment.js
@@ -1146,6 +1146,154 @@ function exitFullscreen() {
}
}
+
+/**
+ * Converts 3D world coordinates to 2D screen coordinates.
+ *
+ * This function takes a 3D vector and converts its coordinates
+ * from the world space to screen space. This can be useful for placing
+ * 2D elements in a 3D scene or for determining the screen position
+ * of 3D objects.
+ *
+ * @method worldToScreen
+ * @param {p5.Vector} worldPosition The 3D coordinates in the world space.
+ * @return {p5.Vector} A vector containing the 2D screen coordinates.
+ * @example
+ *
+ *
+ *
+ * function setup() {
+ * createCanvas(150, 150);
+ * let vertices = [
+ * createVector(-20, -20),
+ * createVector(20, -20),
+ * createVector(20, 20),
+ * createVector(-20, 20)
+ * ];
+ *
+ * push();
+ * translate(75, 55);
+ * rotate(PI / 4);
+ *
+ * // Convert world coordinates to screen coordinates
+ * let screenPos = vertices.map(v => worldToScreen(v));
+ * pop();
+ *
+ * background(200);
+ *
+ * stroke(0);
+ * fill(100, 150, 255, 100);
+ * beginShape();
+ * screenPos.forEach(pos => vertex(pos.x, pos.y));
+ * endShape(CLOSE);
+ *
+ * screenPos.forEach((pos, i) => {
+ * fill(0);
+ * textSize(10);
+ * if (i === 0) {
+ * text(i + 1, pos.x + 3, pos.y - 7);
+ * } else if (i === 1) {
+ * text(i + 1, pos.x + 7, pos.y + 2);
+ * } else if (i === 2) {
+ * text(i + 1, pos.x - 2, pos.y + 12);
+ * } else if (i === 3) {
+ * text(i + 1, pos.x - 12, pos.y - 2);
+ * }
+ * });
+ *
+ * fill(0);
+ * noStroke();
+ * textSize(10);
+ * let legendY = height - 35;
+ * screenPos.forEach((pos, i) => {
+ * text(`Vertex ${i + 1}: (${pos.x.toFixed(1)}, ${pos.y.toFixed(1)})`, 5, legendY + i * 10);
+ * });
+ *
+ * describe('A rotating square is transformed and drawn using screen coordinates.');
+ *
+ * }
+ *
+ *
+ *
+ * @example
+ *
+ *
+ * let vertices;
+ *
+ * function setup() {
+ * createCanvas(100, 100, WEBGL);
+ * vertices = [
+ * createVector(-25, -25, -25),
+ * createVector(25, -25, -25),
+ * createVector(25, 25, -25),
+ * createVector(-25, 25, -25),
+ * createVector(-25, -25, 25),
+ * createVector(25, -25, 25),
+ * createVector(25, 25, 25),
+ * createVector(-25, 25, 25)
+ * ];
+ *
+ * describe('A rotating cube with points mapped to 2D screen space and displayed as ellipses.');
+ *
+ * }
+ *
+ * function draw() {
+ * background(200);
+ *
+ * // Animate rotation
+ * let rotationX = millis() / 1000;
+ * let rotationY = millis() / 1200;
+ *
+ * push();
+ *
+ * rotateX(rotationX);
+ * rotateY(rotationY);
+ *
+ * // Convert world coordinates to screen coordinates
+ * let screenPos = vertices.map(v => worldToScreen(v));
+ *
+ * pop();
+ *
+ * screenPos.forEach((pos, i) => {
+ *
+ * let screenX = pos.x - width / 2;
+ * let screenY = pos.y - height / 2;
+ * fill(0);
+ * noStroke();
+ * ellipse(screenX, screenY, 3, 3);
+ * });
+ * }
+ *
+ *
+ *
+ */
+
+p5.prototype.worldToScreen = function(worldPosition) {
+ const renderer = this._renderer;
+ if (renderer.drawingContext instanceof CanvasRenderingContext2D) {
+ // Handle 2D context
+ const transformMatrix = new DOMMatrix()
+ .scale(1 / renderer._pInst.pixelDensity())
+ .multiply(renderer.drawingContext.getTransform());
+ const screenCoordinates = transformMatrix.transformPoint(
+ new DOMPoint(worldPosition.x, worldPosition.y)
+ );
+ return new p5.Vector(screenCoordinates.x, screenCoordinates.y);
+ } else {
+ // Handle WebGL context (3D)
+ const modelViewMatrix = renderer.calculateCombinedMatrix();
+ const cameraCoordinates = modelViewMatrix.multiplyPoint(worldPosition);
+ const normalizedDeviceCoordinates =
+ renderer.states.uPMatrix.multiplyAndNormalizePoint(cameraCoordinates);
+ const screenX = (0.5 + 0.5 * normalizedDeviceCoordinates.x) * this.width;
+ const screenY = (0.5 - 0.5 * normalizedDeviceCoordinates.y) * this.height;
+ const screenZ = 0.5 + 0.5 * normalizedDeviceCoordinates.z;
+ return new p5.Vector(screenX, screenY, screenZ);
+ }
+};
+
+
+
/**
* Returns the sketch's current
* URL
diff --git a/src/webgl/p5.RendererGL.js b/src/webgl/p5.RendererGL.js
index 4ab0f7f706..f0504d7f91 100644
--- a/src/webgl/p5.RendererGL.js
+++ b/src/webgl/p5.RendererGL.js
@@ -935,6 +935,15 @@ p5.RendererGL = class RendererGL extends Renderer {
this.clear(_r, _g, _b, _a);
}
+ // Combines the model and view matrices to get the uMVMatrix
+ // This method will be reusable wherever you need to update the combined matrix.
+ calculateCombinedMatrix() {
+ const modelMatrix = this.states.uModelMatrix;
+ const viewMatrix = this.states.uViewMatrix;
+ return modelMatrix.copy().mult(viewMatrix);
+ }
+
+
//////////////////////////////////////////////
// COLOR
//////////////////////////////////////////////
diff --git a/src/webgl/p5.Shader.js b/src/webgl/p5.Shader.js
index 68cb21131d..cc1568254a 100644
--- a/src/webgl/p5.Shader.js
+++ b/src/webgl/p5.Shader.js
@@ -937,7 +937,7 @@ function shader(p5, fn){
const viewMatrix = this._renderer.states.uViewMatrix;
const projectionMatrix = this._renderer.states.uPMatrix;
const modelViewMatrix = (modelMatrix.copy()).mult(viewMatrix);
- this._renderer.states.uMVMatrix = modelViewMatrix;
+ this._renderer.states.uMVMatrix = this._renderer.calculateCombinedMatrix();
const modelViewProjectionMatrix = modelViewMatrix.copy();
modelViewProjectionMatrix.mult(projectionMatrix);
diff --git a/test/unit/core/environment.js b/test/unit/core/environment.js
index 9c167797b9..d7d758e1a1 100644
--- a/test/unit/core/environment.js
+++ b/test/unit/core/environment.js
@@ -236,4 +236,61 @@ suite('Environment', function() {
assert.isNumber(myp5.displayDensity(), pd);
});
});
+
+ suite('2D context test', function() {
+ beforeEach(function() {
+ myp5.createCanvas(100, 100);
+ });
+
+ test('worldToScreen for 2D context', function() {
+ let worldPos = myp5.createVector(50, 50);
+ let screenPos = myp5.worldToScreen(worldPos);
+ assert.closeTo(screenPos.x, 50, 0.1);
+ assert.closeTo(screenPos.y, 50, 0.1);
+ });
+
+ test('worldToScreen with rotation in 2D', function() {
+ myp5.push();
+ myp5.translate(50, 50);
+ myp5.rotate(myp5.PI / 2);
+ let worldPos = myp5.createVector(10, 0);
+ let screenPos = myp5.worldToScreen(worldPos);
+ myp5.pop();
+ assert.closeTo(screenPos.x, 50, 0.1);
+ assert.closeTo(screenPos.y, 60, 0.1);
+ });
+ });
+
+ suite('3D context test', function() {
+ beforeEach(function() {
+ myp5.createCanvas(100, 100, myp5.WEBGL);
+ });
+
+ test('worldToScreen for 3D context', function() {
+ let worldPos = myp5.createVector(0, 0, 0);
+ let screenPos = myp5.worldToScreen(worldPos);
+ assert.closeTo(screenPos.x, 50, 0.1);
+ assert.closeTo(screenPos.y, 50, 0.1);
+ });
+
+ test('worldToScreen with rotation in 3D around Y-axis', function() {
+ myp5.push();
+ myp5.rotateY(myp5.PI / 2);
+ let worldPos = myp5.createVector(50, 0, 0);
+ let screenPos = myp5.worldToScreen(worldPos);
+ myp5.pop();
+ assert.closeTo(screenPos.x, 50, 0.1);
+ assert.closeTo(screenPos.y, 50, 0.1);
+ });
+
+ test('worldToScreen with rotation in 3D around Z-axis', function() {
+ myp5.push();
+ myp5.rotateZ(myp5.PI / 2);
+ let worldPos = myp5.createVector(10, 0, 0);
+ let screenPos = myp5.worldToScreen(worldPos);
+ myp5.pop();
+ assert.closeTo(screenPos.x, 50, 0.1);
+ assert.closeTo(screenPos.y, 60, 0.1);
+ });
+ });
});