Related
THREE.js provides high-level 3D graphics, however it does not support 3D curves; it only supports a 2D SplineCurve.
Luckly there is a NURBS manipulator available at npmjs.com, supporting any dimension degree. I think what I need from nurbs is the method NURBS(controlPoints).evaluate().
However I'm not understanding what arguments are expected to this evaluate() method. I want to convert NURBS control points into something THREE.js understands, such as triangles.
import NURBS from 'nurbs';
let curve = NURBS({
points: [[-1, 0, 0], [-0.5, 0.5, 0.5], [0.5, -0.5, -0.5], [1, 0, 0]],
degree: 3,
});
let r = [0, 0, 0];
curve.evaluate(r, 0, 0, 0);
console.log(r); // [ 4.083333333333332, -19.249999999999996, -19.249999999999996 ]
curve.evaluate(r, .1, .1, .1);
console.log(r); // [ 3.5756666666666677, -17.527, -17.527 ]
I'm currently building a 2D drawing app in WebGL. I want to implement zoom to point under mouse cursor similar to example in here. But I can't figure out how to apply the solution from that answer in my case.
I have done basic zoom by scaling camera matrix. But it zooms to the top-left corner of the canvas, due to that being the origin (0,0) set by the projection (as far as I understand).
Basic pan & zoom implemented:
My draw function (including matrix computations) looks like this:
var projection = null;
var view = null;
var viewProjection = null;
function draw(gl, camera, sceneTree){
// projection matrix
projection = new Float32Array(9);
mat3.projection(projection, gl.canvas.clientWidth, gl.canvas.clientHeight);
// camera matrix
view = new Float32Array(9);
mat3.fromTranslation(view, camera.translation);
mat3.rotate(view, view, toRadians(camera.rotation));
mat3.scale(view, view, camera.scale);
// view matrix
mat3.invert(view, view)
// VP matrix
viewProjection = new Float32Array(9);
mat3.multiply(viewProjection, projection, view);
// go through scene tree:
// - build final matrix for each object
// e.g: u_matrix = VP x Model (translate x rotate x scale)
// draw each object in scene tree
// ...
}
Vertex shader:
attribute vec2 a_position;
uniform mat3 u_matrix;
void main() {
gl_Position = vec4((u_matrix * vec3(a_position, 1)).xy, 0, 1);
}
Zoom function:
function screenToWorld(screenPos){
// normalized screen position
let nsp = [
2.0 * screenPos[0] / this.gl.canvas.width - 1,
- 2.0 * screenPos[1] / this.gl.canvas.height + 1
];
let inverseVP = new Float32Array(9);
mat3.invert(inverseVP, viewProjection);
let worldPos = [0, 0];
return vec2.transformMat3(worldPos, nsp, inverseVP);
}
var zoomRange = [0.01, 2];
canvas.addEventListener('wheel', (e) => {
let oldZoom = camera.scale[0];
let zoom = Math.min(Math.max(oldZoom + e.deltaX / 100, zoomRange[0]), zoomRange[1]);
camera.scale = [zoom, zoom];
let zoomPoint = screenToWorld([e.clientX, e.clientY]);
// totally breaks if enable this line
//vec2.copy(camera.translation, zoomPoint);
// call draw function again
draw();
}, false);
If I apply zoomPoint to camera translation, the values of zoomPoint (and the camera position accordingly) start to raise up uncontrollably with every zoom event (no mater if I zoom in or out) and the objects drawn in the scene go immediately out of view.
Would greatly appreciate any insights or suggestions about what am I doing wrong here. Thanks.
Since you didn't post a minimal reproducible example in the question itself I couldn't test with your math library. Using my own though I was able to zoom like this
const [clipX, clipY] = getClipSpaceMousePosition(e);
// position before zooming
const [preZoomX, preZoomY] = m3.transformPoint(
m3.inverse(viewProjectionMat),
[clipX, clipY]);
// multiply the wheel movement by the current zoom level
// so we zoom less when zoomed in and more when zoomed out
const newZoom = camera.zoom * Math.pow(2, e.deltaY * -0.01);
camera.zoom = Math.max(0.02, Math.min(100, newZoom));
updateViewProjection();
// position after zooming
const [postZoomX, postZoomY] = m3.transformPoint(
m3.inverse(viewProjectionMat),
[clipX, clipY]);
// camera needs to be moved the difference of before and after
camera.x += preZoomX - postZoomX;
camera.y += preZoomY - postZoomY;
Note that zoom is the opposite of scale. If zoom = 2 then I want everything to appear 2x larger. To do that requires shrinking the camera space so we scale that space by 1 / zoom
Example:
const canvas = document.querySelector('canvas');
const gl = canvas.getContext('webgl');
const vs = `
attribute vec2 a_position;
uniform mat3 u_matrix;
void main() {
gl_Position = vec4((u_matrix * vec3(a_position, 1)).xy, 0, 1);
}
`;
const fs = `
precision mediump float;
uniform vec4 u_color;
void main() {
gl_FragColor = u_color;
}
`;
// compiles shaders, links program, looks up locations
const programInfo = twgl.createProgramInfo(gl, [vs, fs]);
// calls gl.createBuffer, gl.bindBuffer, gl.bufferData
const bufferInfo = twgl.createBufferInfoFromArrays(gl, {
a_position: {
numComponents: 2,
data: [
0, 0, // 0----1
40, 0, // | |
40, 10, // | 3--2
10, 10, // | |
10, 20, // | 4-5
30, 20, // | |
30, 30, // | 7-6
10, 30, // | |
10, 50, // 9-8
0, 50,
],
},
indices: [
0, 1, 2,
0, 2, 3,
0, 3, 8,
0, 8, 9,
4, 5, 6,
4, 6, 7,
],
});
const camera = {
x: 0,
y: 0,
rotation: 0,
zoom: 1,
};
const scene = [
{ x: 20, y: 20, rotation: 0, scale: 1, color: [1, 0, 0, 1], bufferInfo},
{ x: 100, y: 50, rotation: Math.PI, scale: 0.5, color: [0, 0.5, 0, 1], bufferInfo},
{ x: 100, y: 50, rotation: 0, scale: 2, color: [0, 0, 1, 1], bufferInfo},
{ x: 200, y: 100, rotation: 0.7, scale: 1, color: [1, 0, 1, 1], bufferInfo},
];
let viewProjectionMat;
function makeCameraMatrix() {
const zoomScale = 1 / camera.zoom;
let cameraMat = m3.identity();
cameraMat = m3.translate(cameraMat, camera.x, camera.y);
cameraMat = m3.rotate(cameraMat, camera.rotation);
cameraMat = m3.scale(cameraMat, zoomScale, zoomScale);
return cameraMat;
}
function updateViewProjection() {
// same as ortho(0, width, height, 0, -1, 1)
const projectionMat = m3.projection(gl.canvas.width, gl.canvas.height);
const cameraMat = makeCameraMatrix();
let viewMat = m3.inverse(cameraMat);
viewProjectionMat = m3.multiply(projectionMat, viewMat);
}
function draw() {
gl.clear(gl.COLOR_BUFFER_BIT);
updateViewProjection();
gl.useProgram(programInfo.program);
for (const {x, y, rotation, scale, color, bufferInfo} of scene) {
// calls gl.bindBuffer, gl.enableVertexAttribArray, gl.vertexAttribPointer
twgl.setBuffersAndAttributes(gl, programInfo, bufferInfo);
let mat = m3.identity();
mat = m3.translate(mat, x, y);
mat = m3.rotate(mat, rotation);
mat = m3.scale(mat, scale, scale);
// calls gl.uniformXXX
twgl.setUniforms(programInfo, {
u_matrix: m3.multiply(viewProjectionMat, mat),
u_color: color,
});
// calls gl.drawArrays or gl.drawElements
twgl.drawBufferInfo(gl, bufferInfo);
}
}
draw();
function getClipSpaceMousePosition(e) {
// get canvas relative css position
const rect = canvas.getBoundingClientRect();
const cssX = e.clientX - rect.left;
const cssY = e.clientY - rect.top;
// get normalized 0 to 1 position across and down canvas
const normalizedX = cssX / canvas.clientWidth;
const normalizedY = cssY / canvas.clientHeight;
// convert to clip space
const clipX = normalizedX * 2 - 1;
const clipY = normalizedY * -2 + 1;
return [clipX, clipY];
}
canvas.addEventListener('wheel', (e) => {
e.preventDefault();
const [clipX, clipY] = getClipSpaceMousePosition(e);
// position before zooming
const [preZoomX, preZoomY] = m3.transformPoint(
m3.inverse(viewProjectionMat),
[clipX, clipY]);
// multiply the wheel movement by the current zoom level
// so we zoom less when zoomed in and more when zoomed out
const newZoom = camera.zoom * Math.pow(2, e.deltaY * -0.01);
camera.zoom = Math.max(0.02, Math.min(100, newZoom));
updateViewProjection();
// position after zooming
const [postZoomX, postZoomY] = m3.transformPoint(
m3.inverse(viewProjectionMat),
[clipX, clipY]);
// camera needs to be moved the difference of before and after
camera.x += preZoomX - postZoomX;
camera.y += preZoomY - postZoomY;
draw();
});
canvas { border: 1px solid black; display: block; }
<canvas></canvas>
<script src="https://twgljs.org/dist/4.x/twgl-full.min.js"></script>
<script src="https://webglfundamentals.org/webgl/resources/m3.js"></script>
note that I included camera.rotation just to make sure things worked if rotated. They seem to. Here's one with zoom, pan, and rotate
I have created two objects doubleSquare and doubleSquare1. I expected output Double Line Square Object. I have tried two type of format but not getting the expected output. Please, provide me the solution. I have seen lot of example while doing website analysis. What did I do wrong in the code?
Type 1 (In the below code doubleSquare closepath start with another object draw.How to split? )
function doubleSquareDemoDraw(startX, startY, endX, endY, color){
var widthLength = Math.round(Math.abs(endX - startX));
var heightLength = Math.round(Math.abs(endY - startY));
console.log(widthLength+'::'+heightLength);
var combined = new THREE.Geometry();
var doubleSquare = new THREE.Geometry();
doubleSquare.vertices.push(new THREE.Vector3(startX, startY, 0));
doubleSquare.vertices.push(new THREE.Vector3(endX, startY, 0));
doubleSquare.vertices.push(new THREE.Vector3(endX, endY, 0));
doubleSquare.vertices.push(new THREE.Vector3(startX, endY, 0));
doubleSquare.vertices.push(new THREE.Vector3(startX, startY, 0));
doubleSquare.vertices.push(new THREE.Vector3(startX, startY, 0)); //closepath
var doubleSquare1 = new THREE.Geometry();
doubleSquare1.vertices.push(
new THREE.Vector3(startX + 4, startY- 4 , 0),
new THREE.Vector3(endX -4, startY- 4, 0),
new THREE.Vector3(endX - 4, endY +4, 0),
new THREE.Vector3(startX + 4, endY + 4, 0),
new THREE.Vector3(startX + 4, startY -4 , 0)
);
THREE.GeometryUtils.merge( combined, doubleSquare );
THREE.GeometryUtils.merge( combined, doubleSquare1 );
var display = new THREE.Line(combined, new THREE.LineBasicMaterial({ color: color }));
return display;
}
Type 2 (In the below code only single object is visible instead of Two)
function doubleSquareDraw(startX, startY, endX, endY, color){
var combined = new THREE.Geometry();
var square1 = new THREE.Shape();
square1.moveTo(startX, startY, 0);
square1.lineTo(endX, startY, 0);
square1.lineTo(endX, endY, 0);
square1.lineTo(startX, endY, 0);
square1.lineTo(startX, startY, 0);
var square2 = new THREE.Path();
square2.moveTo(startX + 4 , startY - 4 , 0);
square2.lineTo(endX - 4 , startY - 4 , 0);
square2.lineTo(endX - 4 , endY + 4 , 0);
square2.lineTo(startX + 4 , endY + 4 , 0);
square2.lineTo(startX + 4 , startY - 4 , 0);
square1.holes.push( square2);
var geometry = new THREE.BufferGeometry().setFromPoints(square.getPoints());
var square = new THREE.Line(geometry, new THREE.MeshBasicMaterial({
color: color
}));
return display;
}
If you look into the source code of this example more attentively, then you will see, that the lines for shapes and for their holes are drawn separately.
For example, that shape arcShape:
We set the shape and its hole here
We pass that shape into addShape( arcShape, extrudeSettings, 0x804000, 150, 0, 0, 0, 0, 0, 1 );
Inside addShape we call addLineShape( shape, color, x, y, z, rx, ry, rz, s );, which draws the outer contour line of the shape;
And to draw the hole inside the cirle, we call addLineShape(), passing the path of that hole in the first parameter. Here.
From what I see in your code, you think, if you set a shape and its holes and then create a buffer geometry with new THREE.BufferGeometry().setFromPoints(shape.getPoints()), then you get a contour line of the shape and contour lines of its holes. You're wrong, as the things work different.
I'm trying to draw a complex shape in Three.js using extruded arcs but they just didn't seem to be behaving properly. I don't know if I don't understand the API, but shouldn't this create a complete extruded circle of radius 100 centred at the origin?
var path = new THREE.Path();
path.moveTo(0, 0);
path.arc(0, 0, 100, 0, Math.PI * 2, false);
var shape = path.toShapes(false, false);
var extrudeSettings = {
amount : 20,
steps : 1
};
var geometry = new THREE.ExtrudeGeometry(shape, extrudeSettings);
var mesh = new THREE.Mesh(geometry, material);
Instead it draws a Pacman shape:
Here's the JSFiddle:
http://jsfiddle.net/c8shqzpn/
You want to create a circle shape so you can extrude it.
Whenever you draw an arc, it connects the start of the arc to the current point, so in your case, you have to use the moveTo() command to set the start point on the perimeter of the circle.
var shape = new THREE.Shape();
shape.moveTo( circleRadius, 0 );
shape.absarc( 0, 0, circleRadius, 0, 2 * Math.PI, false );
three.js r.70
I had similar issues drawing 3/4 of a circle and extruding it and adding the result (a THREE.Mesh) to the screen. The circle seemed to miss a segment. Adding moveTo( x, y ) where x, y are coordinates of the beginning of the arc solved the issue. I used this code:
var extrudeSettings = {
bevelEnabled: false,
steps: 1,
amount: 2
};
var shape = new THREE.Shape();
var circleRadius = 4;
// THIS LINE SOLVES THE ISSUE
shape.moveTo( 0, -circleRadius );
shape.absarc( 0, 0, circleRadius, 0, 1.5 * Math.PI, false );
shape.lineTo( 0, 0 );
shape.closePath();
var geometry = shape.extrude( extrudeSettings );
scene.add( new THREE.Mesh( geometry, new THREE.MeshNormalMaterial() ) );
First my mesh looked like this:
After adding shape.moveTo( 0, -circleRadius ); the mesh looked like this:
Could it be solved if you multiply Math.PI bye 2.1, instead of 2?
I wrote the code below to get intersection point with a 3d shape. It works well but if there are two intersection point with shape, it returns me only the farest intersection while I need the nearest intersection with the shape. How can I do to get the nearest intersection?
/*here I create a cube*/
var geometry0 = new THREE.Geometry()
geometry0.vertices = [new THREE.Vector3(0.5, -0.5, 0.5), new THREE.Vector3(-0.5, -0.5, 0.5), new THREE.Vector3(-0.5, -0.5, -0.5), new THREE.Vector3(0.5, -0.5, -0.5), new THREE.Vector3(0.5, 0.5, 0.5), new THREE.Vector3(-0.5, 0.5, 0.5), new THREE.Vector3(-0.5, 0.5, -0.5), new THREE.Vector3(0.5, 0.5, -0.5)];
geometry0.faces = [new THREE.Face3(3, 2, 1), new THREE.Face3(3, 1, 0), new THREE.Face3(4, 5, 6), new THREE.Face3(4, 6, 7), new THREE.Face3(0, 1, 5), new THREE.Face3(0, 5, 4), new THREE.Face3(1, 2, 6), new THREE.Face3(1, 6, 5), new THREE.Face3(2, 3, 7), new THREE.Face3(2, 7, 6), new THREE.Face3(3, 0, 4), new THREE.Face3(3, 4, 7)];
geometry0.computeFaceNormals();
geometry0.computeVertexNormals();
var material0 = new THREE.MeshBasicMaterial({color: 0x39d2dbe7fff39d2, transparent: true, opacity: 0});
mesh0 = new THREE.Mesh(geometry0, material0);
egh0 = new THREE.EdgesHelper(mesh0, 0x000);
egh0.material.linewidth = 2;
scene.add(egh0);
objects.push(mesh0);
projector = new THREE.Projector();
console.log(objects);
mouse2D = new THREE.Vector3(0, 10000, 0.5);//controllare i valori
/* here I create the ray */
document.addEventListener('click', onDocumentMouseClick, false);
function onDocumentMouseClick(event) {
event.preventDefault();
mouse2D.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse2D.y = -(event.clientY / window.innerHeight) * 2 + 1;
var vector = new THREE.Vector3( mouse2D.x, mouse2D.y, 0.5 );
projector.unprojectVector( vector, camera );
var raycaster = new THREE.Raycaster( camera.position, vector.sub( camera.position ).normalize() );
var intersects = raycaster.intersectObjects( objects );
if (intersects.length > 0) {
console.log("ok");}
If I check intersects[0].point I can only see the farest point face of cube intersected and not the first (for example,if you look at a cube in front of you and make a ray,this ray before intersect the first face and after intersect second face behind the first.)
The goal of this code is to create an event only when you click on vertices.
So after this I wrote code to calculate the euclidean distance from point of click and all vertices
and return the vertice nearest to the point of click.
If you have other idea to fire an event only when you click on vertices is welcome.
Yes, the basic idea of ray casting is that we project a ray perpendicular to the plane we find out the list of objects that the ray has intersected.
So all you have to do to access the first element is adding the following piece of code.
var intersects = raycaster.intersectObjects(objects);
if (intersects.length > 0) {
var firstIntersectedObject = intersects[0];
// this will give you the first intersected Object if there are multiple.
}
Here is one of my other SO post in which I have explained things in a bit more detailed way, you can refer it to better understand how raycasting functions.
Try to make through this example. Look at messages in the console.
<script src="js/controls/EventsControls.js"></script>
EventsControls = new EventsControls( camera, renderer.domElement );
EventsControls.draggable = false;
EventsControls.onclick = function() {
console.log( this.focused.name );
console.log( 'this.focusedPoint: (' + this.focusedPoint.x + ', ' +
this.focusedPoint.y + ', ' + this.focusedPoint.z + ')' );
console.log( 'this.focusedDistance: ' + this.focusedDistance );
}
var mesh = new THREE.Mesh( geometry, material );
scene.add( mesh );
EventsControls.attach( mesh );
//
function render() {
EventsControls.update();
controls.update();
renderer.render(scene, camera);
}