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); + }); + }); });