How can I bind two shapes together as one in Three.js? - javascript

Can I bind two different shapes together as one shape?
For example, binding sphere and cylinder together as one?

Kind of, yes. There are multiple options:
via hierarchy you can simply add one mesh to another using the add() function
via the GeometryUtil's merge() function to merge vertices and meshes of two Geometry objects into one
using a basic 3D editor that supports Boolean operations between meshes and exporting.
Method 1 is pretty straightforward:
var sphere = new THREE.Mesh(new THREE.SphereGeometry(100, 16, 12), new THREE.MeshLambertMaterial({ color: 0x2D303D, wireframe: true, shading: THREE.FlatShading }));
var cylinder = new THREE.Mesh(new THREE.CylinderGeometry(100, 100, 200, 16, 4, false), new THREE.MeshLambertMaterial({ color: 0x2D303D, wireframe: true, shading: THREE.FlatShading } ));
cylinder.position.y = -100;
scene.add(sphere);
scene.add(cylinder);
Notice that 16 is repeated, so the subdivisions level in one mesh matches the other (for a decent look).
Method 2.1 - via GeometryUtils
// Make a sphere
var sg = new THREE.SphereGeometry(100, 16, 12);
// Make a cylinder - ideally the segmentation would be similar to predictable results
var cg = new THREE.CylinderGeometry(100, 100, 200, 16, 4, false);
// Move vertices down for cylinder, so it maches half the sphere - offset pivot
for(var i = 0 ; i < cg.vertices.length; i++)
cg.vertices[i].position.y -= 100;
// Merge meshes
THREE.GeometryUtils.merge(sg, cg);
var mesh = new THREE.Mesh(sg, new THREE.MeshLambertMaterial({ color: 0x2D303D, wireframe: true, shading: THREE.FlatShading }));
scene.add(mesh);
Method 2.2 merging a Lathe half-sphere and a cylinder:
var pts = []; // Points array
var detail = .1; // Half-circle detail - how many angle increments will be used to generate points
var radius = 100; // Radius for half_sphere
var total = Math.PI * .51;
for(var angle = 0.0; angle < total ; angle+= detail) // Loop from 0.0 radians to PI (0 - 180 degrees)
pts.push(new THREE.Vector3(0,Math.cos(angle) * radius,Math.sin(angle) * radius)); // Angle/radius to x,z
var lathe = new THREE.LatheGeometry(pts, 16); // Create the lathe with 12 radial repetitions of the profile
// Rotate vertices in lathe geometry by 90 degrees
var rx90 = new THREE.Matrix4();
rx90.setRotationFromEuler(new THREE.Vector3(-Math.PI * .5, 0, 0));
lathe.applyMatrix(rx90);
// Make cylinder - ideally the segmentation would be similar for predictable results
var cg = new THREE.CylinderGeometry(100, 100, 200, 16, 4, false);
// Move vertices down for cylinder, so it maches half the sphere
for(var i = 0 ; i < cg.vertices.length; i++)
cg.vertices[i].position.y -= 100;
// Merge meshes
THREE.GeometryUtils.merge(lathe, cg);
var mesh = new THREE.Mesh(lathe, new THREE.MeshLambertMaterial({ color: 0x2D303D, wireframe: true, shading: THREE.FlatShading}));
mesh.position.y = 150;
scene.add(mesh);
The one problem I can't address at the moment comes from the faces that are inside the mesh. Ideally, those would have normals flipped, so they wouldn't render, but I haven't found a quick solution for that.
The third is fairly straightforward. Most 3D packages allow Boolean operation on meshes (e.g., merging two meshes together with the ADD operation (meshA + meshB)). Try creating a cylinder and a sphere in Blender (free and opensource), which already has a Three.js exporter. Alternatively you can export an .obj file of the merged meshes from your 3D editor or choice and use the convert_obj_three script.
I've found yet another method, which might be easier/more intuitive. Remember the Boolean operations I've mentioned above?
It turns out there is an awesome JavaScript library just for that: Constructive Solid Geometry:
Chandler Prall wrote some handy functions to connect CSG with three.js. So with the CSG library and the Three.js wrapper for it, you can simply do this:
var cylinder = THREE.CSG.toCSG(new THREE.CylinderGeometry(100, 100, 200, 16, 4, false), new THREE.Vector3(0, -100, 0));
var sphere = THREE.CSG.toCSG(new THREE.SphereGeometry(100, 16, 12));
var geometry = cylinder.union(sphere);
var mesh = new THREE.Mesh(THREE.CSG.fromCSG(geometry), new THREE.MeshNormalMaterial());
Which gives you a nice result (no problems with extra faces/flipping normals, etc.):

Related

Creating three plane geometry perpendicular to x, y and z axis?

I am supposed to create three plane geometries to the scene (one perpendicular to x axis, one perpendicular to y axis and one perpendicular to z axis) that would somewhat look like this.
So, herein is my code below for the same:
public showPlaneGeometry() {
console.log(this.hScene)
const geometryx = new th.PlaneGeometry(1, 1);
const geometryy = new th.PlaneGeometry(1, 1);
const geometryz = new th.PlaneGeometry(1, 1);
const material = new th.MeshBasicMaterial({
color: 0xa6cfe2,
side: th.DoubleSide,
transparent: true,
opacity: 0.5,
depthWrite: false,
});
const planex = new th.Mesh(geometryx, material);
const planey = new th.Mesh(geometryy, material);
const planez = new th.Mesh(geometryz, material);
planex.position.set(1, 0, 0);
planey.position.set(0, 1, 0)
planez.position.set(0, 0, 1)
material.transparent = true
this.hScene.add(planex, planey, planez);
}
The output for the same is just appearing this way disappointingly:
I was wondering how do I make it look like the image I posted above? That they intersect at the centre that way and the surface meshes when added to the scene would appear at the centre intersecting point? Thank you so much for your help in advance.
You don't need to change their position, since they all should go through 0, 0, 0. However, you need to change their rotation if you want them to align along different axes.
const planeGeom = new THREE.PlaneGeometry(1, 1);
const planeMat = new THREE.MeshBasicMaterial({
color: 0xa6cfe2,
side: THREE.DoubleSide,
transparent: true,
opacity: 0.5,
depthWrite: false,
});
const planeXY = new THREE.Mesh(planeGeom, planeMat);
const planeXZ = new THREE.Mesh(planeGeom, planeMat);
const planeYZ = new THREE.Mesh(planeGeom, planeMat);
// Default plane already occupies XY plane
planeXY.rotation.set(0, 0, 0);
// Rotate around x-axis to occupy XZ plane
planeXZ.rotation.set(Math.PI / 2, 0, 0);
// Rotate around y-axis to occupy YZ plane
planeYZ.rotation.set(0, Math.PI / 2, 0);
There's also no need to create 3 different plane geometries. You can just re-use the same one 3 times.

Create a curved grid for threejs

I want to create a grid of, let's say 500x500, and I want it to be able to be curved in certain places (think of it as space-time plane that's curved due to gravity). I'm stuck at the start.
This is what I found on the documentation:
// Create a sine-like wave
const curve = new THREE.SplineCurve( [
new THREE.Vector2( -10, 0 ),
new THREE.Vector2( -5, 5 ),
new THREE.Vector2( 0, 0 ),
new THREE.Vector2( 5, -5 ),
new THREE.Vector2( 10, 0 )
] );
const points = curve.getPoints( 50 );
const geometry = new THREE.BufferGeometry().setFromPoints( points );
const material = new THREE.LineBasicMaterial( { color : 0xff0000 } );
// Create the final object to add to the scene
const splineObject = new THREE.Line( geometry, material );
I think this should be working but it doesn't. I don't know how to create a multiple lines from here. I tried to handle some array but I didn't know where or how. I have done my research but I can't make any headway.
I have another question: Is that this is a Vector2 I need to make it 3d for that work? There a lot of other classes like CatmullRomCurve3, CubicBezierCurve3, etc, but the problem still is that i need to make an array to create all the vectors and lines.
--
EDIT i created a code for the grid lines but the curve probleme still the same
let size = 12, step = 1;
const geometry = new THREE.BufferGeometry();
// create a simple square shape. We duplicate the top left and bottom right
// vertices because each vertex needs to appear once per triangle.
const vertices = [];
for(var i = - size; i <= size; i += step) {
vertices.push( - size, - 0.4, i);
vertices.push( size, - 0.4, i);
vertices.push( i, - 0.4, - size);
vertices.push( i, - 0.4, size);
}
let positionAttribute = new THREE.Float32BufferAttribute(vertices, 3);
geometry.setAttribute("position", positionAttribute);
let lines = new THREE.LineSegments(geometry, new THREE.LineBasicMaterial());

convert bezier into a plane road in three.js

I am trying to draw a curved road in three.js from some beziers I get with previous calculations, the problem is that I can't find the way to convert the sequence of curved lines (one starting at the end of the previous one) to a curved plane.
I have a 3D scene where there are some cars, a road created with a plane and the path of the coming road is painted. I use that Bezier curves I said to represent the path as a Line with
function createAdasisBezier(initx, inity, cp1x, cp1y, cp2x, cp2y, finalx, finaly) {
bezier = new THREE.CubicBezierCurve3(
new THREE.Vector3(initx, inity, 0),
new THREE.Vector3(cp1x, cp1y, 0),
new THREE.Vector3( cp2x, cp2y, 0),
new THREE.Vector3(finalx, finaly, 0)
);
curvePath = new THREE.CurvePath();
curvePath.add(bezier);
var geoPath = curvePath.createPointsGeometry( 5 );
var lineMat = new THREE.LineBasicMaterial({color: 0xff0000});
curveLine = new THREE.Line(geoPath, lineMat);
curveLine.rotation.set(-Math.PI/2,0,0);
curveLine.position.y = 0.1;
scene.add(curveLine);
}
First, I tried extruding the line, but then I realized that it might not be the solution because I wanted to do a road, and although I could move top vertices on X and Y to place them near the bezier in order to be the external part of the curve, the result was not only unfavourable, it also made impossible to preserve a relation between a left and a right curve.
To move vertices (once identified) I did a loop and move them manually:
for (var i = 0; i < geoPath.vertices.length; ++i) {
geoPath.vertices[i].y += 10;
}
Bevel is not enabled in the extude.
Then I tried to draw a plane over each bezier (as a child of them) and rotate it to face the path, but the result was not as I expected, and it if it were, it would spoil the arcs of the curves.
To do it, I created a copy of every bezier, and place it aside the original ones, then I created the plane.
var plane = new THREE.PlaneBufferGeometry(10,25,1,1);
var planemesh = new THREE.Mesh(plane, material);
planemesh.position.set(copy.geometry.vertices[0].x, copy.geometry.vertices[0].y, 0);
Last thing I was trying to do is creating a clone of the line, separate it some meters and "connect" the first vertex from one, to the first of the other, so I get a closed geometry, and I can create a Face, but I don't find how to "connect" vertices from 2 different geometries. I tried adding the vertex from one to the other, but it did not work.
Does anybody have an idea how could I convert the line into a curved road? Thanks in adcance.
You should try looking at the Geometry > Extrude > Shapes example. As you can see, all extruded shapes maintain their width and direction, despite turning left/right or looping completely.
Instead of using bezier curves, they're using a CatmullRomCurve3 to define the extrusion. If you look at the source code, the essential code to make the red extruded shape begins in line 69:
// Define the curve
var closedSpline = new THREE.CatmullRomCurve3( [
new THREE.Vector3( -60, -100, 60 ),
new THREE.Vector3( -60, 20, 60 ),
new THREE.Vector3( -60, 120, 60 ),
new THREE.Vector3( 60, 20, -60 ),
new THREE.Vector3( 60, -100, -60 )
] );
closedSpline.type = 'catmullrom';
closedSpline.closed = true;
// Set up settings for later extrusion
var extrudeSettings = {
steps : 100,
bevelEnabled : false,
extrudePath : closedSpline
};
// Define a triangle
var pts = [], count = 3;
for ( var i = 0; i < count; i ++ ) {
var l = 20;
var a = 2 * i / count * Math.PI;
pts.push( new THREE.Vector2 ( Math.cos( a ) * l, Math.sin( a ) * l ) );
}
var shape = new THREE.Shape( pts );
// Extrude the triangle along the CatmullRom curve
var geometry = new THREE.ExtrudeGeometry( shape, extrudeSettings );
var material = new THREE.MeshLambertMaterial( { color: 0xb00000, wireframe: false } );
// Create mesh with the resulting geometry
var mesh = new THREE.Mesh( geometry, material );
From here, it should only be a matter of small tweaks to these parameters to get the specific road shape you want.

Three.js Multiple Materials in Plane Geometry

I am trying to use a single plane to show multiple materials. I am currently using the MultiMaterial with the materials I plan to use inside of (textured).
The issue I am having is that the materials I use seem to get split across the entire plane into little chunks for each face. However I would like to have a material cover a 1 / n amount of the plane/mesh.
My current code (shortened):
var splitX = 2;
var splitY = 2;
var geometry = new THREE.PlaneGeometry(800, 800, splitX, splitY);
var materials = [
new THREE.MeshBasicMaterial({
map: preloaded texture..., side: THREE.DoubleSide
}),
new THREE.MeshBasicMaterial({
color: 0xff0000, side: THREE.DoubleSide
})
];
// set a single square inside the plane to the desired textured material
geometry.faces[0].materialIndex = 0;
geometry.faces[1].materialIndex = 0;
// set the other squares inside the plane to use the coloured material
for(var i = 1; i < geometry.faces.length / 2; i++) {
geometry.faces[i * 2].materialIndex = 1;
geometry.faces[i * 2 + 1].materialIndex = 1;
}
var mesh = new THREE.Mesh(geometry, new THREE.MultiMaterial(materials));
scene.add(mesh);
The output: http://prnt.sc/e8un2a
I marked each corner of the texture to see whether it would show the entire texture in the 2 faces I specified and it did not. Any help would be appreciated to resolve this! :)

Threejs PlaneGeometry doesn't receive shadows or reflections

I'm pretty new to 3d and to threejs and I can't figure out how I can get a PlaneGeometry to show individually illuminated polygons i.e. receive shadows or show reflection. What I basically do is taking a PlaneGeometry applying some noise to every z value of the vertices. Then I have a simple directional light in my scene which is supposed to make the emerging noise pattern on the plane visible. I tried different things like plane.castShadow = true or renderer.shadowMapEnabled = true without success. Am I just missing a simple option or is this way more complicated than I think?
Here's are the relevant pieces of my code
renderer.setSize(width, height);
renderer.setClearColor(0x111111, 1);
...
var directionalLight = new THREE.DirectionalLight( 0xffffff, 0.9);
directionalLight.position.set(10, 2, 20);
directionalLight.castShadow = true;
directionalLight.shadowCameraVisible = true;
scene.add( directionalLight );
var geometry = new THREE.PlaneGeometry(20, 20, segments, segments);
var index = 0;
for(var i=0; i < segments + 1; i++) {
for(var j=0; j < segments + 1; j++) {
zOffset = simplex.noise2D(i * xNoiseScale, j * yNoiseScale) * 5;
geometry.vertices[index].z = zOffset;
index++;
}
}
var material = new THREE.MeshLambertMaterial({
side: THREE.DoubleSide,
color: 0xf50066
});
var plane = new THREE.Mesh(geometry, material);
plane.rotation.x = -Math.PI / 2.35;
plane.castShadow = true;
plane.receiveShadow = true;
scene.add(plane);
This is the output I get. Obviously the plane is aware of the light because the bottom side is darker than the upper side but there is no sign of any individual polygons receiving individual lightening and no 3d structure is visible. Interestingly when I put in a different geometry like a BoxGeometry individual polygons are illuminated individually (see 2nd image). Any ideas?
Ok I figured it out thanks to this post. The trick is to use the THREE.FlatShading shader on the material. Important to note is that after every update of the vertices two things need to be done. Before rendering geometry.normalsNeedUpdate must be set to true so the renderer also incorporates the newly oriented vertices. Also geometry.computeFaceNormals() needs to be called before rendering because when you alter the vertices the normals are not the same anymore.

Categories