I'm trying to construct a collection of flat shapes in three.js. Each one is defined as a series of coplanar Vector3 points, but the shapes are not all coplanar. Imagine two flat rectangles as the roof of a house, but with much more complex shapes.
I can make flat Shape objects and then rotate and position them, but since my shapes are conceived in 3d coordinates, it would be much simpler to keep it all in 3-space, which the Shape object doesn't like.
Is there some much more direct way to simply specify an array of coplanar Vector3's, and let three.js do the rest of the work?
I thought about this problem and came up with the idea, when you have a set of co-planar points and you know the normal of the plane (let's name it normal), which your points belong to.
We need to rotate our set of points to make it parallel to the xy-plane, thus the normal of that plane is [0, 0, 1] (let's name it normalZ). To do it, we find quaternions with .setFromUnitVectors() of THREE.Quaternion():
var quaternion = new THREE.Quaternion().setFromUnitVectors(normal, normalZ);
var quaternionBack = new THREE.Quaternion().setFromUnitVectors(normalZ, normal);
Apply quaternion to our set of points
As it's parallel to xy-plane now, z-coordinates of points don't matter, so we can now create a THREE.Shape() object of them. And then create THREE.ShapeGeometry() (name it shapeGeom) from given shape, which will triangulate our shape.
We need to put our points back to their original positions, so we'll apply quaternionBack to them.
After all, we'll assign our set of points to the .vertices property of the shapeGeom.
That's it. If it'll work for you, let me know ;)
var scene = new THREE.Scene();
var camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 1, 1000);
camera.position.set(0, 20, 40);
camera.lookAt(scene.position);
var renderer = new THREE.WebGLRenderer({
antialias: true
});
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
var controls = new THREE.OrbitControls(camera, renderer.domElement);
controls.target = new THREE.Vector3(10, 0, 10);
controls.update();
var grid = new THREE.GridHelper(50, 50, 0x808080, 0x202020); // xy-grid
grid.geometry.rotateX(Math.PI * 0.5);
scene.add(grid);
var points = [ // all of them are on the xz-plane
new THREE.Vector3(5, 0, 5),
new THREE.Vector3(25, 0, 5),
new THREE.Vector3(25, 0, 15),
new THREE.Vector3(15, 0, 15),
new THREE.Vector3(15, 0, 25),
new THREE.Vector3(5, 0, 25),
new THREE.Vector3(5, 0, 5)
]
var geom = new THREE.BufferGeometry().setFromPoints(points);
var pointsObj = new THREE.Points(geom, new THREE.PointsMaterial({
color: "red"
}));
scene.add(pointsObj);
var line = new THREE.LineLoop(geom, new THREE.LineBasicMaterial({
color: "aqua"
}));
scene.add(line);
// normals
var normal = new THREE.Vector3(0, 1, 0); // I already know the normal of xz-plane ;)
scene.add(new THREE.ArrowHelper(normal, new THREE.Vector3(10, 0, 10), 5, 0xffff00)); //yellow
var normalZ = new THREE.Vector3(0, 0, 1); // base normal of xy-plane
scene.add(new THREE.ArrowHelper(normalZ, scene.position, 5, 0x00ffff)); // aqua
// 1 quaternions
var quaternion = new THREE.Quaternion().setFromUnitVectors(normal, normalZ);
var quaternionBack = new THREE.Quaternion().setFromUnitVectors(normalZ, normal);
// 2 make it parallel to xy-plane
points.forEach(p => {
p.applyQuaternion(quaternion)
});
// 3 create shape and shapeGeometry
var shape = new THREE.Shape(points);
var shapeGeom = new THREE.ShapeGeometry(shape);
// 4 put our points back to their origins
points.forEach(p => {
p.applyQuaternion(quaternionBack)
});
// 5 assign points to .vertices
shapeGeom.vertices = points;
var shapeMesh = new THREE.Mesh(shapeGeom, new THREE.MeshBasicMaterial({
color: 0x404040
}));
scene.add(shapeMesh);
render();
function render() {
requestAnimationFrame(render);
renderer.render(scene, camera);
}
body {
overflow: hidden;
margin: 0;
}
<script src="https://cdn.jsdelivr.net/npm/three#0.90.0/build/three.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three#0.90.0/examples/js/controls/OrbitControls.js"></script>
Related
I used to have a custom square in my project that was made by using THREE.Geometry. Now after the latest update of three.js, Geometry was completely deleted and I have to get my code to work with BufferGeometry instead.
I'm kind of confused how I have to change the code to make it work. I don't really get the vertices part now. I used to have 4 vertices made of Vector3's and two faces.
So it looked like that:
let v1 = new THREE.Vector3(...,...,...)
let v2 = new THREE.Vector3(...,...,...)
let v3 = new THREE.Vector3(...,...,...)
let v4 = new THREE.Vector3(...,...,...)
obj.vertices.push(v1)
obj.vertices.push(v2)
obj.vertices.push(v3)
obj.vertices.push(v4)
let face1 = new THREE.Face3(0, 1, 2)
let face2 = new THREE.Face3(2, 3, 0)
obj.faces.push(face1)
obj.faces.push(face2)
I've read the documentation on that on the THREE website but I don't really understand how I change that to make it work with BufferGeometry. Any tips would be appreciated.
Thanks
Try it like so:
let camera, scene, renderer;
init();
render();
function init() {
camera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 0.01, 10);
camera.position.z = 2;
scene = new THREE.Scene();
const geometry = new THREE.BufferGeometry();
const vertices = [
-1, -1, 0,
1, -1, 0,
1, 1, 0,
-1, 1, 0
];
const indices = [
0, 1, 2, // first triangle
2, 3, 0 // second triangle
];
geometry.setAttribute('position', new THREE.Float32BufferAttribute(vertices, 3));
geometry.setIndex(indices);
const material = new THREE.MeshBasicMaterial();
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
renderer = new THREE.WebGLRenderer({antialias: true});
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
}
function render() {
renderer.render(scene, camera);
}
body {
margin: 0;
}
<script src="https://cdn.jsdelivr.net/npm/three#0.126.1/build/three.js"></script>
How do you combine a Square and a Rectangle into one object that a Raycaster can detect successfully?
I created a custom “Tree” object by making a “Trunk” - which is just a long rectangle, and then sticking a Square object on top of that Trunk.
I then “planted” that Tree on top of a Sphere, and I'm trying to get my raycaster to detect it.
It’s not working.
Here’s my code:
// My custom “Tree” Object:
var Tree = function(treeColor) {
this.mesh = new THREE.Object3D();
this.mesh.name = "tree";
// I start with the TRUNK - which is just an elongated Cube - meaning a Rectangle:
var trunkGeometry = new THREE.CubeGeometry(0.2, 0.5, 0.2);
var trunkMaterial = new THREE.MeshBasicMaterial({ color: "blue", wireframe: false });
var treeTrunk = new THREE.Mesh(trunkGeometry, trunkMaterial);
treeTrunk.position.set(0, 0, 0);
treeTrunk.rotation.x = -Math.PI * 0.5;
this.mesh.add(treeTrunk);
// Then I create the FOLIAGE - which is just a regular Cube:
var foliageGeometry = new THREE.CubeGeometry(0.5, 0.5, 0.5);
var foliageMaterial = new THREE.MeshBasicMaterial({ color: treeColor, wireframe: false });
var treeFoliage = new THREE.Mesh(foliageGeometry, foliageMaterial);
treeFoliage.position.set(0, 0.5, 0);
// And then I attach/add the FOLIAGE to the TRUNK:
treeTrunk.add(treeFoliage);
}
// Next I make a basic Sphere:
theSun = new THREE.Mesh(new THREE.SphereGeometry(3, 32, 24), new THREE.MeshBasicMaterial( {color: "white", wireframe: false} ));
scene.add(theSun);
// And then I make a Tree and add it to my Sphere (`theSun`):
var oneTree = new Tree("red");
let rx = Math.random() * Math.PI * 2;
let ry = Math.random() * Math.PI;
oneTree.mesh.position.setFromSphericalCoords(3.2, ry, rx);
oneTree.mesh.lookAt(theSun.position);
theSun.add(oneTree.mesh);
// Next I add both theSun and the Tree to my “objectsForRayCasterArray” - a global var I use in my raycaster test:
objectsForRayCasterArray.push(oneTree);
objectsForRayCasterArray.push(theSun);
// In my render() function, I do the usual raycasting business:
rayCaster = new THREE.Raycaster();
function render() {
requestAnimationFrame(render);
controls.update();
renderer.render(scene, camera);
// Raycasting stuff:
rayCaster.setFromCamera(mousePosition, camera);
var intersectingObjectsArray = rayCaster.intersectObjects(objectsForRayCasterArray);
if (intersectingObjectsArray.length > 0) {
if (intersectedObject != intersectingObjectsArray[0].object) {
console.log(“Intersected!”);
}
}
NOTE: when I use this same code but instead of a Tree I just place a regular Cube on my Sphere object, everything works just fine. The raycaster detects the Cube and fires the Alert.
My best guess is that since you're nesting the treeFoliage inside treeTrunk, and that inside mesh, you're going to have to use a recursive intersection test.
According to the docs, intersectObjects() accepts a second argument to perform a recursive hit-test. This means it will iterate through all descendants, instead of just doing a shallow check of top-level objects:
rayCaster.intersectObjects(objectsForRayCasterArray, true);
How to get the bounding sphere for a whole scene in three.js?
I may try to get the bounding sphere for each object and compute the resulting union of them, but I think there may be a more straight forward method.
There are different methods to get a boundingSphere of multiple objects dynamically. You can get first the bounding box of all of them, and then create a sphere of that bounding box... here is a sample fiddle I have shaped on boundingSphere of a boundingBox.
Basically you put all the geometries into a Group, you get the Box3 of the group, and then you do getBoundingSphere from the Box3 and position at the center. Code in the fiddle would be this.
let g = new THREE.Group();
scene.add(g);
for (let i = 0; i < 5; i++) {
// geometry
var geometry = new THREE.BoxGeometry(20, 20, 20);
// material
var material = new THREE.MeshToonMaterial({
color: 0xff0000,
opacity: 0.7,
});
// mesh
mesh = new THREE.Mesh(geometry, material);
mesh.position.set(100 * Math.random(), 100 * Math.random(), 100 * Math.random());
g.add(mesh);
}
//g.updateWorldMatrix(true);
var gridHelper = new THREE.GridHelper(400, 40, 0x0000ff, 0x808080);
gridHelper.position.y = 0;
gridHelper.position.x = 0;
scene.add(gridHelper);
let bbox = new THREE.Box3().setFromObject(g);
let helper = new THREE.Box3Helper(bbox, new THREE.Color(0, 255, 0));
scene.add(helper);
const center = new THREE.Vector3();
bbox.getCenter(center);
let bsphere = bbox.getBoundingSphere(new THREE.Sphere(center));
let m = new THREE.MeshStandardMaterial({
color: 0xffffff,
opacity: 0.3,
transparent: true
});
var geometry = new THREE.SphereGeometry(bsphere.radius, 32, 32);
let sMesh = new THREE.Mesh(geometry, m);
scene.add(sMesh);
sMesh.position.copy(center);
EDITED: If you want to include in the boundingSphere for the scene including the lights (which could get you a huge sphere), just start from let bbox = new THREE.Box3().setFromObject(scene)
A rotated object (cylinder in this case) cuts off objects (a triangle made by lines in this case) even though the renderOrder of the second object is higher. See this jsfiddle demo for the effect.
The triangle should be rendered completely on top of the cylinder but is cut off where the outside of the cylinder intersects with it. It's easier to understand what's happening when a texture is used, but jsfiddle is bad at using external images.
var mesh, renderer, scene, camera, controls;
init();
animate();
function init() {
renderer = new THREE.WebGLRenderer({
antialias: true,
preserveDrawingBuffer: true
});
renderer.setClearColor(0x24132E, 1);
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
scene = new THREE.Scene();
camera = new THREE.PerspectiveCamera(40, window.innerWidth / window.innerHeight, 0.1, 10000);
camera.position.set(0, 0, 7);
camera.lookAt(scene.position)
scene.add(camera);
var geometry = new THREE.CylinderGeometry(1, 1, 100, 32, 1, true);
var material = new THREE.MeshBasicMaterial({
color: 0x0000ff
});
material.side = THREE.DoubleSide;
mesh = new THREE.Mesh(geometry, material);
mesh.rotation.x = Math.PI / 2;
scene.add(mesh);
var c = 3, // Side length of the triangle
a = c / 2,
b = Math.sqrt(c * c - a * a),
yOffset = -b / 3; // The vertical offset (if 0, triangle is on x axis)
// Draw the red triangle
var geo = new THREE.Geometry();
geo.vertices.push(
new THREE.Vector3(0, b + yOffset, 0),
new THREE.Vector3(-a, 0 + yOffset, 0),
new THREE.Vector3(a, 0 + yOffset, 0),
new THREE.Vector3(0, b + yOffset, 0)
);
var lineMaterial = new THREE.LineBasicMaterial({
color: 0xff0000,
linewidth: 5,
linejoin: "miter"
});
plane = new THREE.Line(geo, lineMaterial);
// Place it on top of the cylinder
plane.renderOrder = 2; // This should override any clipping, right?
scene.add(plane);
}
function animate() {
requestAnimationFrame(animate);
render();
}
function render() {
renderer.render(scene, camera);
}
Am I doing something wrong or is this a bug?
for the effect that you want use a second scene and render it onto the first one
function init(){
.....
renderer.autoClear = false;
scene.add(tube);
overlayScene.add(triangle);
.....
}
function render() {
renderer.clear();
renderer.render(scene, camera);
renderer.clearDepth();
renderer.render(overlayScene, camera);
}
renderOrder does not mean what you think it means, look at the implementation in WebGLRenderer
objects are sorted by the order, if it meant what you anticipated from it, there would always be some fixed rendering order and colliding objects would be seen through each other, renderOrder is AFAIK used when you have issues with order of transparent/ not opaque objects
I worte a little plugin for three.js for flares for my game. Three.js built-in flares plugin is slow and I preferred not to run another rendering pass which was cutting framerate in half. Here's how I got flares visible on top of objects which were actually in front of them.
Material parameters:
{
side: THREE.FrontSide,
blending: THREE.AdditiveBlending,
transparent: true,
map: flareMap,
depthWrite: false,
polygonOffset: true,
polygonOffsetFactor: -200
}
depthWrite - set to false
polygonOffset - set to true
polygonOffsetFactor - give negative number to get object in front of others. Give it some really high value to be really on top of everything i.e. -10000
Ignore other params, they are needed for my flares
For my project I need collision tests in Three.js. In my CollisionDetection class I'm trying to get a Raycaster to work. And I found some weirdness that I can't explain and can't find a way around:
My CollisionDetector works fine for Cubes.. but when I use Spheres instead, it doesn't give me the same results – Am I wrong to expect the same results as for the cubes? Or do I miss something else?
Here is my Code:
var renderer, camera, scene;
init();
animate();
function init() {
var container = document.getElementById("scene");
var width = window.innerWidth;
var height = window.innerHeight;
renderer = new THREE.WebGLRenderer();
renderer.setSize(width, height);
camera = new THREE.OrthographicCamera( 0, width, 0, height, 1, 10000 );
camera.position.z = 300;
scene = new THREE.Scene();
scene.add(camera);
container.appendChild(renderer.domElement);
var geometry = new THREE.SphereGeometry(10,16, 16);
//var geometry = new THREE.CubeGeometry( 10, 10, 10 );
var material1 = new THREE.MeshBasicMaterial( { color: 0xFF3333} );
var material2 = new THREE.MeshBasicMaterial( { color: 0xFF3333} );
var material3 = new THREE.MeshBasicMaterial( { color: 0xFF3333} );
var material4 = new THREE.MeshBasicMaterial( { color: 0xFF3333} );
var material5 = new THREE.MeshBasicMaterial( { color: 0xFF3333} );
var element1 = new THREE.Mesh( geometry, material1 );
var element2 = new THREE.Mesh( geometry, material2 );
var element3 = new THREE.Mesh( geometry, material3 );
var element4 = new THREE.Mesh( geometry, material4 );
var element5 = new THREE.Mesh( geometry, material5 );
element1.position.set(200,200,0);
element2.position.set(200,100,0);
element3.position.set(200,300,0);
element4.position.set(100,200,0);
element5.position.set(300,200,0);
scene.add(element1);
scene.add(element2);
scene.add(element3);
scene.add(element4);
scene.add(element5);
var CollisionDetector = new CollisionDetection();
CollisionDetector.addRay(new THREE.Vector3(0, -1, 0));
CollisionDetector.addRay(new THREE.Vector3(0, 1, 0));
CollisionDetector.addRay(new THREE.Vector3(1, 0, 0));
CollisionDetector.addRay(new THREE.Vector3(-1, 0, 0));
CollisionDetector.addElement(element1);
CollisionDetector.addElement(element2);
CollisionDetector.addElement(element3);
CollisionDetector.addElement(element4);
CollisionDetector.addElement(element5);
document.onclick = function(){
CollisionDetector.testElement(element1);
};
}
function CollisionDetection(){
var caster = new THREE.Raycaster();
var rays = [];
var elements = [];
this.testElement = function(element){
for(var i=0; i<rays.length; i++) {
caster.set(element.position, rays[i]);
var hits = caster.intersectObjects(elements, true);
for(var k=0; k<hits.length; k++) {
console.log("hit", hits[k]);
hits[k].object.material.color.setHex(0x0000ff);
}
}
}
this.addRay = function(ray) {
rays.push(ray.normalize());
}
this.addElement = function(element){
elements.push(element);
}
}
function animate() {
requestAnimationFrame( animate );
renderer.render( scene, camera );
}
Or best, see for yourself how it behaves: http://jsfiddle.net/mymL5/12/
On Click every element hit by a ray should turn blue and all hits are registered in the console.
Note the (imho) weird console output for spheres.
Also, why is the lower sphere not hit while the upper is?
You can switch between Cubes and Spheres by Commenting/Uncommenting lines 19/20
Can anyone help me? What am I not getting?
PS: I'm new to Three.js, so I'm probably being dumb.
Since this is homework-related, I am only going to provide some tips.
Your scene is rendering upside down because your args to orthographic camera are incorrect.
Your sphere is bigger than your cube.
Your rays are hitting the north and south poles of your spheres exactly. What is different about those points?
The material.side property tells Raycaster which side(s) of a face to consider the "front".
Your fidde example is running an old version (r.54) of three.js.
three.js r.58
Increased spheres size.
Rotated spheres by some non-trivial angle (so they don't get hit right in the N/S pole).
Now it works? :P
var geometry = new THREE.SphereGeometry(20,17, 17);
element1.position.set(0,0,0);
element2.position.set(0,100,0);
element3.position.set(100,0,0);
element4.position.set(0,-100,0);
element5.position.set(-100,0,0);
element1.rotation.set(0,0,10);
element2.rotation.set(0,0,10);
element3.rotation.set(0,0,10);
element4.rotation.set(0,0,10);
element5.rotation.set(0,0,10);
Still, ray test should be aware of the hitting exact vertex or edge of the triangle, so that might be considered as a place-to-improve for Three.js.
I filed an issue about this in the Three.js repository:
https://github.com/mrdoob/three.js/issues/3541