I have built this snake game where the snakes are animated around using a bunch of cube meshes and THREE.BoxGeometry:
I would prefer each snake to consist of just one mesh and one geometry so that I can easily add textures, rounded edges etc.
I'm trying to take a set of 3d points and convert them into a single geometry that resembles the box-like snakes in the demo (like several THREE.BoxGeometry attached together).
I've tried to achieve that using THREE.CurvePath and THREE.ExtrudeGeometry:
_makeSnakeGeometry(positions) {
const vectors = positions.map(p => new THREE.Vector3(...p));
const curvePath = new THREE.CurvePath();
const first = vectors[1];
const last = vectors[vectors.length - 1];
let curveStart = vectors[0];
let previous = curveStart;
let previousDirection;
// Walk through the positions. If there is a change in direction, add
// a new curve to the curve path.
for (let vector of vectors.slice(1)) {
let direction = previous.clone().sub(vector);
if (vector.equals(first)) {
curveStart.sub(direction.clone().divideScalar(2));
}
if (vector.equals(last)) {
previous.sub(previousDirection.clone().divideScalar(2));
}
if ((previousDirection && !previousDirection.equals(direction)) || vector.equals(last)) {
curvePath.add(new THREE.LineCurve3(curveStart, previous));
curveStart = previous;
}
previous = vector;
previousDirection = direction;
}
const p = Const.TILE_SIZE / 2;
const points = [[-p, -p], [-p, p], [p, p], [p, -p]];
const shape = new THREE.Shape(points.map(p => new THREE.Vector2(...p)));
const geometry = new THREE.ExtrudeGeometry(shape, {
steps: 20,
extrudePath: curvePath,
amount: 20
});
return geometry;
}
Unfortunately, it looks quite ugly and we end up with rounded corners where the path direction changes:
If I increase the steps option, the resulting mesh looks less ugly but the geometry has a large amount of vertices due to the smooth curved edges. I'm concerned about a large vertex count because I may have to recreate this geometry on every animation frame.
To animate the geometry, I've tried to use geometry.morphTargets with little success. The morph happens but it, too, looks quite ugly. Perhaps I need to manually animate the vertices of the geometry?
To summarize, my questions are:
How can I create a geometry with minimal vertices that resembles several box geometries pieced together?
What is the correct way to animate a geometry when the underlying vertex count can change?
To build the snake geometry, I ended up merging geometries:
_makeSnakeGeometry(positions) {
positions = positions.map(p => new THREE.Vector3(...p));
const geometry = new THREE.Geometry();
for (let i = 0; i < positions.length; i += 1) {
const position = positions[i];
const mesh = makeVoxelMesh(Const.TILE_SIZE, { position });
mesh.updateMatrix();
geometry.merge(mesh.geometry, mesh.matrix);
}
return geometry;
}
makeVoxelMesh returns a single cube mesh for one position of the snake. I join them together using geometry.merge and repeat the process on every animation frame. It's not perfect because the geometry ends up having extra vertices and faces that it doesn't need to have for such a simple shape. Maybe there is a way to reduce them after the fact?
To animate the snake, I find the four vertices that comprise the head face. I do it by looking at the vertex positions and face normals:
_findHeadFaceVertices() {
const direction = this._direction.clone().multiplyScalar(1 + (Const.TILE_SIZE / 10));
const headFacePosition = new THREE.Vector3(...this.head).add(direction);
const { vertices, faces } = this.mesh.geometry;
// Sort vertices by distance to a point near the snake head and only keep
// the first few that are equidistant.
const closest = vertices
.map(v => [v.distanceTo(headFacePosition), v])
.sort((a, b) => a[0] - b[0])
.filter((pair, i, sorted) => pair[0] === sorted[0][0])
.map(pair => pair[1]);
const result = [];
const seen = {};
// Filter the remaining vertices by keeping only ones which belong to faces
// that point in the same direction as the snake.
for (let face of faces) {
for (let vertex of [vertices[face.a], vertices[face.b], vertices[face.c]]) {
const key = vertex.toArray().toString();
if (!seen[key] && closest.includes(vertex) && face.normal.equals(this._direction)) {
seen[key] = true;
result.push(vertex);
}
}
}
return result;
}
For animation to work, I had to set this.mesh.geometry.verticesNeedUpdate = true; after each animation frame.
Related
I have an axis, as defined by 2 vectors, for example one that points upwards at x = 10:
const axisStart = new Vector3(10, 0, 0)
const axisEnd = new Vector3(10, 0, 1)
I'm getting the normalized axis direction like so:
const axisDirection = new Vector3().subVectors(axisEnd, axisStart).normalize()
How can I rotate a vector (e.g. Vector3(50, 0, 0)) around my original axis?
I've tried using Vector3.applyAxisAngle(axisDirection , radians), but because the axis has been normalized, the rotation happens around the world center (0, 0) and not around the axis' original position.
I've solved this by finding the exact point on the axis around which the point rotates, using this answer and translating the pseudocode from it into typescript:
getPivotPoint(pointToRotate: Vector3, axisStart: Vector3, axisEnd: Vector3) {
const d = new Vector3().subVectors(axisEnd, axisStart).normalize()
const v = new Vector3().subVectors(pointToRotate, axisStart)
const t = v.dot(d)
const pivotPoint = axisStart.add(d.multiplyScalar(t))
return pivotPoint
}
Then, as #Ouroborus pointed out, I can then translate the point, apply the rotation, and translate it back:
rotatePointAroundAxis(pointToRotate: Vector3, axisStart: Vector3, axisEnd, radians: number) {
const axisDirection = new Vector3().subVectors(axisEnd, axisStart).normalize()
const pivotPoint = getPivotPoint(pointToRotate, axisStart, axisEnd)
const translationToWorldCenter = new Vector3().subVectors(pointToRotate, pivotPoint)
const translatedRotated = translationToWorldCenter.clone().applyAxisAngle(axisDirection, radians)
const destination = pointToRotate.clone().add(translatedRotated).sub(translationToWorldCenter)
return destination
}
The above code is working nicely, leaving it here for my future self and for other who might find this useful.
I am working in autodesk forge which includes Threejs r71 and I want to use a raycaster to detect clicks on different elements within a pointcloud.
Sample code for how to do this with ThreeJs r71 be appreciated.
Right now, I register an extension with the forge api and run the code below within it. It creates creates a pointcloud and positions the points at predetermined locations (saved within the cameraInfo array).
let geometry = new THREE.Geometry();
this.cameraInfo.forEach( function(e) {
geometry.vertices.push(e.position);
}
)
const material = new THREE.PointCloudMaterial( { size: 150, color: 0Xff0000, sizeAttenuation: true } );
this.points = new THREE.PointCloud( geometry, material );
this.scene.add(this.points);
/* Set up event listeners */
document.addEventListener('mousemove', event => {
// console.log('mouse move!');
let mouse = {
x: ( event.clientX / window.innerWidth ) * 2 - 1,
y: - ( event.clientY / window.innerHeight ) * 2 + 1
};
let raycaster = new THREE.Raycaster();
raycaster.params.PointCloud.threshold = 15;
let vector = new THREE.Vector3(mouse.x, mouse.y, 0.5).unproject(this.camera);
raycaster.ray.set(this.camera.position, vector.sub(this.camera.position).normalize());
this.scene.updateMatrixWorld();
let intersects = raycaster.intersectObject(this.points);
if (intersects.length > 0) {
const hitIndex = intersects[0].index;
const hitPoint = this.points.geometry.vertices[ hitIndex ];
console.log(hitIndex);
console.log(hitPoint);
}
}, false);
The output seems to be illogical. At certain camera positions, it will constantly tell me that it is intersecting an item in the pointcloud (regardless of where the mouse is). And at certain camera positions, it won't detect an intersection at all.
TLDR: it doesn't actually detect an intersection b/w my pointcloud and the mouse.
I've simplified the code a bit, using some of the viewer APIs (using a couple of sample points in the point cloud):
const viewer = NOP_VIEWER;
const geometry = new THREE.Geometry();
for (let i = -100; i <= 100; i += 10) {
geometry.vertices.push(new THREE.Vector3(i, i, i));
}
const material = new THREE.PointCloudMaterial({ size: 50, color: 0Xff0000, sizeAttenuation: true });
const points = new THREE.PointCloud(geometry, material);
viewer.impl.scene.add(points);
const raycaster = new THREE.Raycaster();
raycaster.params.PointCloud.threshold = 50;
document.addEventListener('mousemove', function(event) {
const ray = viewer.impl.viewportToRay(viewer.impl.clientToViewport(event.clientX, event.clientY));
raycaster.ray.set(ray.origin, ray.direction);
let intersects = raycaster.intersectObject(viewer.impl.scene, true);
if (intersects.length > 0) {
console.log(intersects[0]);
}
});
I believe you'll need to tweak the raycaster.params.PointCloud.threshold value. The ray casting logic in three.js doesn't actually intersect the point "boxes" that you see rendered on the screen. It only computes distance between the ray and the point (in the world coordinate system), and only outputs an intersection when the distance is under the threshold value. In my example I tried setting the threshold to 50, and the intersection results were somewhat better.
As a side note, if you don't necessarily need point clouds inside the scene, consider overlaying HTML elements over the 3D view instead. We're using the approach in the https://forge-digital-twin.autodesk.io demo (source) to show rich annotations attached to specific positions in the 3D space. With this approach, you don't have to worry about custom intersections - the browser handles everything for you.
I'm trying to create a geometry from a quickhull of a THREE Mesh, but the QuickHull instance only appears to have information relevant to the Faces.
Is there a way to get each vertex information from this instance?
Thank you in advance.
const hull = new QuickHull().setFromObject(mesh) //Mesh is an already rendered object
//hull.vertices //this returns the entire geometry instead of the hull's vertices
Yes, that should be possible. Try it like so:
const hull = new THREE.QuickHull().setFromObject( mesh );
const vertices = [];
const faces = quickHull.faces;
for ( let i = 0; i < faces.length; i ++ ) {
const face = faces[ i ];
let edge = face.edge;
do {
const point = edge.head().point;
vertices.push( point.x, point.y, point.z );
edge = edge.next;
} while ( edge !== face.edge );
}
As you can see, the idea is to use the half-edges of the faces in order collect all vertices in the correct order.
I am supposed to plot the well deviation surveys on a 3D grid. With the help of a few articles on the web, I have accomplished a 3D grid with required size. The current problem I am facing right now is that the labels for x,y and z axis are attached to the grid, rather they are misplaced on the scene.
var labelsH = labelAxis(height, data.labels.y,"y");
labelsH.position.x = width;
labelsH.position.y = - height +(2*height/a)-20;
labelsH.position.z = depth;
scene.add(labelsH);
function labelAxis(width, data, direction){
var separator = 2*width/data.length,
p = {
x:0,
y:0,
z:0
},
dobj = new THREE.Object3D();
for ( var i = 0; i < data.length; i ++ ) {
var label = makeTextSprite(data[i]);
label.position.set(p.x,p.y,p.z);
dobj.add( label );
if (direction=="y"){
p[direction]+=separator;
}else{
p[direction]-=separator;
}
//console.log(p.x+":"+p.y+":"+p.z)
}
return dobj;
}
See the https://jsfiddle.net/3tw3dt1u/ for full code example.
Further more, the data that I need to plot is already in the jsFiddle mentioned above. Having minimal javascript skills, I have no idea how this data will be plotted on the grid to form something like this:
see image for required result
Thanks a lot in advance for any help.
Your question is regarding the plotting of the points, this is how it could be done:
JSFiddle working example
The key points are below.
// Not necessary, but I prefer to use the same scale as they use in their example. It's also easier since the data is according to those scales.
var graphDimensions = {
w:3000,
d:3000,
h:7000
};
var vectors = realData.map(function(d) { // Create vectors from your data
return new THREE.Vector3(d[0], d[1], d[2]);
});
var curve = new THREE.CatmullRomCurve3(vectors); // Create a curve with the vectors created above
var material = new THREE.LineBasicMaterial({color: "blue"}); // Material for the curve
var geometry = new THREE.Geometry();
var splinePoints = curve.getPoints(5000); // The 5000 in here is the resolution (number of points) on the curve. Increase for a smoother curve, decrease for a more jagged curve.
for (var i = 0; i < splinePoints.length; i++) { // Loop through the points to create vertices in the geometry
geometry.vertices.push(splinePoints[i]);
}
var line = new THREE.Line(geometry, material); // Create a line with your geometry and material
scene.add(line); // Add it to the scene
line.rotateX(Math.PI / 2); // Orient the line correctly
boundingGrid.position.set(0, -3.5, 1.5); // Move the grid into position
boundingGrid.scale.set(0.001, 0.001, 0.001); // Reduce by whatever scale you want to decrease it in size (otherwise you have to scroll out forever)
line.scale.set(0.001, 0.001, 0.001);
I have an IcosahedronGeometry defined like this (with all the code about colors and non-position stuff omitted):
var radius = 200;
geometry = new THREE.IcosahedronGeometry(radius, 2);
var materials = [
new THREE.MeshPhongMaterial({}),
new THREE.MeshBasicMaterial({})
];
group1 = THREE.SceneUtils.createMultiMaterialObject(geometry, materials);
group1.position.x = 0;
// rotate a bit just so it spins off-axis
group1.rotation.x = -1.87;
Which creates an almost spherical, many-sided shape.
I want to place little spheres at just a few of the vertices of this shape. Let's say 10 spheres. I do this by copying 10 vertices into an array, like this:
var vertexArray = [];
for (var i = 0; i < 10; i++) {
vertexArray.push(geometry4.vertices[i])
}
then, I use the Vectors copied into vertexArray to set the positions off Sprites:
for (var i = 0; i < vertexArray.length; i++) {
var loader = new THREE.TextureLoader()
var spriteMaterial = new THREE.SpriteMaterial(
{
map: loader.load('/glow.png'),
blending: THREE.AdditiveBlending,
side: THREE.BackSide
})
var sprite = new THREE.Sprite(spriteMaterial)
sprite.position.set(vertexArray[i].x, vertexArray[i].y, vertexArray[i].z)
// do i need rotate these by the same amount?
sprite.rotation.x = -1.87
scene.add(sprite)
}
This all works fine, except that the Sprites don't line up with the actual vertices on the Icosahedron, they just sit randomly (seemingly) somewhere on the faces. Occasionally a Sprite will sit exactly on a vertex.
Am I copying the vertices wrong, or missing a step in here?
Add an object as a child of a parent object, and the child object will rotate with the parent.
Use this pattern, instead:
var sprite = new THREE.Sprite( spriteMaterial );
sprite.position.copy( vertexArray[i] );
group1.add( sprite );
three.js r.76