Convert global coordinates to local - matrix

A particle system based on three.js' Points.
Internally I am treating the particles as global (with position and velocity vectors) and update the Points geometry accordingly.
It works nicely if the Points object is global (and static).
I need to change this so the Points object moves through the Scene (as child of the "particle emitter"). That requires converting global coordinates for each particle to local coordinates for the Points object geometry.
I have attempted the following:
local = globalParticle.position.clone().applyMatrix4( movingPointsObject.matrixWorld.invert() );
and
local = movingPointsObject.worldToLocal( globalParticle.position );
with different results.
The former seems to work in principle, but the particles appear to have duplicates depending on the Z-rotation of the Points object (they align when rotation is PI).
The latter causes the particles to rotate quickly, giving the appearance of a ring.
What's going on?
https://jsfiddle.net/b7nLorvf/
or
const renderer = new THREE.WebGLRenderer();
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(480, 480);
document.body.appendChild(renderer.domElement);
const scene = new THREE.Scene();
scene.background = new THREE.Color("gray");
const camera = new THREE.OrthographicCamera(-2, 2, 2, -2, 1, 2);
camera.position.set(0, 0, 2);
scene.add(camera);
const light = new THREE.AmbientLight(0xffffff, 0.75); // soft white light
scene.add(light);
const bgeometry = new THREE.BoxGeometry(0.1, 0.1, 0.1);
const bmaterial = new THREE.MeshBasicMaterial({
color: 0xff00ff
});
const cube = new THREE.Mesh(bgeometry, bmaterial);
scene.add(cube);
let pgeometry = new THREE.BufferGeometry();
//pgeometry.setAttribute("position", new THREE.Float32BufferAttribute([], 3));
const pmaterial = new THREE.PointsMaterial({
color: 0xff0000,
size: 2
}); //, depthTest:true } );
let points = new THREE.Points(pgeometry, pmaterial);
const axesHelper = new THREE.AxesHelper(1);
points.add(axesHelper);
//const global = true;
const global = false;
if (global) {
scene.add(points); // global
} else {
cube.add(points); // local
}
let particles = [];
let lt = 0,
dt = 0;
function animate(ms) {
dt = (ms - lt) / 1000;
lt = ms;
// move emitter
const a = ms / 1000;
cube.position.x = Math.cos(a) * 1;
cube.position.y = Math.sin(a) * 1;
cube.rotation.z = a;
// particles
particles.push({
position: cube.position.clone(),
velocity: new THREE.Vector3(2, 0, 0).applyEuler(cube.rotation),
life: 1
});
for (let p of particles) {
p.life -= dt;
}
particles = particles.filter(p => {
return p.life > 0.0;
});
let v = [];
for (let p of particles) {
p.position.add(p.velocity.clone().multiplyScalar(dt));
let vertex;
if (global) {
vertex = p.position;
} else {
//vertex = p.position.clone().applyMatrix4( points.matrixWorld.invert() );
vertex = points.worldToLocal( p.position );
}
v.push(vertex.x, vertex.y, vertex.z);
}
pgeometry.setAttribute("position", new THREE.Float32BufferAttribute(v, 3));
renderer.render(scene, camera);
requestAnimationFrame(animate);
}
requestAnimationFrame(animate);
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/0.148.0/three.min.js"></script>

The former works in conjunction with
movingPointsObject.updateWorldMatrix(true, true);
Inspired by worldToLocal, see three.js/Object3D.js#L256.

Related

Three.js Move an object to front of camera

Hello I'm trying to move an object to front of camera, and when it reached to target position, I want it to stop. but it doesn't work.
function objectToCamera(mX, mY, object)
{
var vector = new THREE.Vector3(mX, mY, 1);
vector.unproject(camera);
vector.sub(object.position);
var dx = object.position.x - camera.position.x;
var dy = object.position.y - camera.position.y;
var dz = object.position.z - camera.position.z;
var distance = Math.sqrt(dx*dx + dy*dy + dz*dz);
if(lastDistance < distance && lastDistance != -1)
keepOut = -1;
lastDistance = distance;
setTimeout(function(){
if( distance > 200 && keepOut == 1)
{
var amount = (1)*(indexForZoom/3);
amount = (amount>15) ? 15 : (1)*(indexForZoom/3);
if(distance - amount < 200)
amount = (distance-200)+1;
indexForZoom++;
object.translateZ(amount);
controls.target.addVectors(controls.target,vector.setLength(amount));
objectToCamera(mX, mY, object)
}
else
{
// stopForZoom = 1;
keepOut = -1;
objectClickHandler(object.name, object);
}
}, 10);
}
I'm checking the distance between camera and object, and if target distance has reached I'm letting it stop, but it doesn't work.
In coordinates, if i'm in positive X coordinates, distance is decreasing, and otherwise, distance is increasing.
I think, in my codes, distance should be decreasing always, but it is not.
Please help. Thanks.
you can use object.position.lerp(target, amount) to move an object toward target. Amount is a value from 0 to 1 with 1 = 100% all the way to target and 0.5 = 50% way to target.
If you want to move at a fixed speed then you can get the distance to the target
distance = object.position.distanceTo(target);
Say you want a max of 0.1 units per interation. then
moveSpeed = 0.1;
distance = object.position.distanceTo(target);
amount = Math.min(moveSpeed, distance) / distance;
object.position.lerp(target, amount)
All that's left is for you to choose a target.
The position in front of the camera is
const distanceFromCamera = 3; // 3 units
const target = new THREE.Vector3(0, 0, -distanceToCamera);
target.applyMatrix4(camera.matrixWorld);
So for example if you move the camera (drag with mouse, use scrollwheel). Note: in the code the speed is adjusted to be frame rate independent.
function main() {
const canvas = document.querySelector('#c');
const renderer = new THREE.WebGLRenderer({canvas});
const fov = 45;
const aspect = 2; // the canvas default
const near = 0.1;
const far = 1000;
const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
camera.position.set(0, 10, 20);
const controls = new THREE.OrbitControls(camera, canvas);
controls.target.set(0, 0, 0);
controls.update();
const scene = new THREE.Scene();
scene.background = new THREE.Color('lightblue');
{
const color = 0xFFFFFF;
const intensity = 1;
const light = new THREE.DirectionalLight(color, intensity);
light.position.set(0, 10, 0);
light.target.position.set(-5, 0, 0);
scene.add(light);
scene.add(light.target);
}
const gridHelper = new THREE.GridHelper(100, 10);
scene.add(gridHelper);
gridHelper.position.set(0, -5, 0);
const cube = new THREE.Mesh(
new THREE.BoxBufferGeometry(1, 1, 1),
new THREE.MeshPhongMaterial({color: 'red'}),
);
scene.add(cube);
function resizeRendererToDisplaySize(renderer) {
const canvas = renderer.domElement;
const width = canvas.clientWidth;
const height = canvas.clientHeight;
const needResize = canvas.width !== width || canvas.height !== height;
if (needResize) {
renderer.setSize(width, height, false);
}
return needResize;
}
let then = 0;
function render(now) {
now *= 0.001; // convert to seconds
const deltaTime = now - then;
then = now;
if (resizeRendererToDisplaySize(renderer)) {
const canvas = renderer.domElement;
camera.aspect = canvas.clientWidth / canvas.clientHeight;
camera.updateProjectionMatrix();
}
cube.rotation.x = now;
cube.rotation.y = now * 1.1;
// move cube in front of camera
{
const distanceFromCamera = 3; // 3 units
const target = new THREE.Vector3(0, 0, -distanceFromCamera);
target.applyMatrix4(camera.matrixWorld);
const moveSpeed = 15; // units per second
const distance = cube.position.distanceTo(target);
if (distance > 0) {
const amount = Math.min(moveSpeed * deltaTime, distance) / distance;
cube.position.lerp(target, amount);
cube.material.color.set('green');
} else {
cube.material.color.set('red');
}
}
renderer.render(scene, camera);
requestAnimationFrame(render);
}
requestAnimationFrame(render);
}
main();
body { margin: 0; }
#c { width: 100vw; height: 100vh; display: block; }
<script src="https://threejsfundamentals.org/threejs/resources/threejs/r112/build/three.min.js"></script>
<script src="https://threejsfundamentals.org/threejs/resources/threejs/r112/examples/js/controls/OrbitControls.js"></script>
<canvas id="c"></canvas>
Note, you might want to call camera.updateMatrixWorld() before all that math to make sure the target isn't one frame late.
If the object is in a hierarchy then there's more to do. You can do the math or you can use just attach the object to the scene and then attach it it back to its place in the hierarchy
const parent = object.parent;
// move object to scene without changing it's world orientation
scene.attach(object);
// do stuff above
// move object to parent without changing it's world orientation
parent.attach(object);

Unable to cast a shadow with THREE.js and Mapbox GL

I'm trying to add a THREE.js scene into a Mapbox GL visualization following this example. I've added a sphere and a ground plane and a DirectionalLight. Now I'm trying to get the light to cast a shadow on the ground plane. Adding a DirectionalLightHelper and a CameraHelper for the light's shadow camera, everything looks pretty reasonable to me:
I'd expect to see a shadow for the sphere on the plane.
Full code here, but here are the highlights:
class SpriteCustomLayer {
type = 'custom';
renderingMode = '3d';
constructor(id) {
this.id = id;
this.gui = new dat.GUI();
THREE.Object3D.DefaultUp.set(0, 0, 1);
}
async onAdd(map, gl) {
this.camera = new THREE.Camera();
const centerLngLat = map.getCenter();
this.center = MercatorCoordinate.fromLngLat(centerLngLat, 0);
const {x, y, z} = this.center;
this.cameraTransform = new THREE.Matrix4()
.makeTranslation(x, y, z)
.scale(new THREE.Vector3(1, -1, 1));
this.map = map;
this.scene = this.makeScene();
this.renderer = new THREE.WebGLRenderer({
canvas: map.getCanvas(),
context: gl,
antialias: true,
});
this.renderer.shadowMap.enabled = true;
this.renderer.autoClear = false;
}
makeScene() {
const scene = new THREE.Scene();
scene.add(new THREE.AmbientLight(0xffffff, 0.25));
const s = this.center.meterInMercatorCoordinateUnits();
const light = new THREE.DirectionalLight(0xffffff, 1);
light.position.set(0.000002360847837325531, 0.000004566603480958114, 0.00000725142167844218);
light.target.position.set(0, 0, 0);
light.castShadow = true;
light.shadow.mapSize.width = 1024;
light.shadow.mapSize.height = 1024;
light.shadow.camera.left = -0.000002383416166278454 * 2;
light.shadow.camera.right = 0.000002383416166278454 * 2;
light.shadow.camera.bottom = -0.000002383416166278454 * 2;
light.shadow.camera.top = 0.000002383416166278454 * 2;
light.shadow.camera.near = 0.0000012388642793465356;
light.shadow.camera.far *= s;
scene.add(light);
this.light = light;
{
const planeSize = 500;
const loader = new THREE.TextureLoader();
const texture = loader.load('/checker.png');
texture.wrapS = THREE.RepeatWrapping;
texture.wrapT = THREE.RepeatWrapping;
texture.magFilter = THREE.NearestFilter;
const repeats = 10;
texture.repeat.set(repeats, repeats);
const planeGeo = new THREE.PlaneBufferGeometry(planeSize, planeSize);
const planeMat = new THREE.MeshPhongMaterial({
map: texture,
side: THREE.DoubleSide,
});
const plane = new THREE.Mesh(planeGeo, planeMat);
plane.scale.setScalar(s);
plane.receiveShadow = true;
scene.add(plane);
}
{
const sphereRadius = 5e-7;
const sphereGeo = new THREE.SphereBufferGeometry(sphereRadius, 32, 32);
const sphereMat = new THREE.MeshPhongMaterial({color: '#CA8'});
const mesh = new THREE.Mesh(sphereGeo, sphereMat);
mesh.position.set(0, 0, 5e-6);
mesh.castShadow = true;
mesh.receiveShadow = false;
sphereMat.side = THREE.DoubleSide;
scene.add(mesh);
}
return scene;
}
render(gl, matrix) {
this.camera.projectionMatrix = new THREE.Matrix4()
.fromArray(matrix)
.multiply(this.cameraTransform);
this.renderer.state.reset();
this.renderer.render(this.scene, this.camera);
this.map.triggerRepaint();
}
}
Mapbox GL JS uses a coordinate system where the entire world is in [0, 1] so the coordinates are pretty tiny. It also uses x/y for lat/lng and z for up, which is different than usual Three.js coordinates.
How can I get the shadow to appear? I'm using Three.js r109 and Mapbox GL JS 1.4.0. I've tried replacing the PlaneBufferGeometry with a thin BoxGeometry to no avail.
EDIT
Forget everything I said in my old answer.
The example below scales things WAY down and the shadow remains.
The kicker was here:
shadowLight.shadow.camera.near *= (scaleDown) ? 0.1 : 10;
shadowLight.shadow.camera.far *= (scaleDown) ? 0.1 : 10;
shadowLight.shadow.camera.updateProjectionMatrix(); // <========= !!!!!
I was updating the scale, but wasn't updating the near/far of the shadow camera. Then, once I was, I was forgetting to update that camera's projection matrix. With all the pieces back together, it seems to be working well.
Try adding a call to update the shadow-casting light's camera's projection matrix after you configure the values.
If it still doesn't work, maybe you can use my example to figure out what's going on in your code.
If MY example doesn't work for you, then it might be your hardware doesn't support the level of precision you need.
// just some random colors to show it's actually rendering
const colors = [
0xff0000, // 1e+1
0x00ff00, // 1e+0
0x0000ff, // 1e-1
0xffff00, // 1e-2
0xff00ff, // 1e-3
0x00ffff, // 1e-4
0xabcdef, // 1e-5
0xfedcba, // 1e-6
0x883300, // 1e-7
0x008833, // 1e-8
0x330088, // 1e-9
0x338800 // 1e-10
];
const renderer = new THREE.WebGLRenderer({
antialias: true
});
renderer.shadowMap.enabled = true; // turn on shadow mapping
renderer.setClearColor(0xcccccc);
document.body.appendChild(renderer.domElement);
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(28, 1, 1, 1000)
camera.position.set(25, 10, 15);
camera.lookAt(0, 0, 0);
const camLight = new THREE.PointLight(0xffffff, 1);
camera.add(camLight);
const floor = new THREE.Mesh(
new THREE.PlaneBufferGeometry(50, 50),
new THREE.MeshPhongMaterial({
color: "gray"
})
);
floor.receiveShadow = true;
floor.rotation.set(Math.PI / -2, 0, 0);
floor.position.set(0, -1, 0);
const sphere = new THREE.Mesh(
new THREE.SphereBufferGeometry(2, 16, 32),
new THREE.MeshPhongMaterial({
color: colors[0]
})
);
sphere.castShadow = true;
sphere.position.set(0, 1, 0);
const shadowLight = new THREE.PointLight(0xffffff, 1);
shadowLight.castShadow = true;
shadowLight.position.set(-10, 10, 5);
const group = new THREE.Group();
group.add(floor);
group.add(sphere);
group.add(shadowLight);
group.add(camera);
scene.add(group);
function render() {
renderer.render(scene, camera);
}
function resize() {
const W = window.innerWidth;
const H = window.innerHeight;
renderer.setSize(W, H);
camera.aspect = W / H;
camera.updateProjectionMatrix();
}
window.onresize = resize;
resize();
render();
let scaler = 10;
let scaleLevel = 10;
let scaleLevelOutput = document.getElementById("scaleLevel");
let scaleDown = true;
let colorIndex = 0;
setInterval(() => {
colorIndex += (scaleDown) ? 1 : -1;
scaleLevel *= (scaleDown) ? 0.1 : 10;
shadowLight.shadow.camera.near *= (scaleDown) ? 0.1 : 10;
shadowLight.shadow.camera.far *= (scaleDown) ? 0.1 : 10;
shadowLight.shadow.camera.updateProjectionMatrix();
if (scaleLevel < 1e-9 && scaleDown) {
scaleDown = false;
}
if (scaleLevel >= 10 && !scaleDown) {
scaleDown = true;
}
scaleLevelOutput.innerText = `SCALE LEVEL: ${scaleLevel.toExponential()}`;
group.scale.set(scaleLevel, scaleLevel, scaleLevel);
sphere.material.color.setHex(colors[colorIndex]);
sphere.material.needsUpdate = true;
render();
}, 1000);
body {
margin: 0;
overflow: hidden;
}
#scaleLevel {
font-family: monospace;
font-size: 2em;
position: absolute;
top: 0;
left: 0;
font-weight: bold;
margin: 5px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/109/three.min.js"></script>
<div id="scaleLevel">SCALE LEVEL: 1e+1</div>

decay and distance with physically correct lighting in three.js

What does the distance setting mean in three.js in relation to physically based lighting?
For non physically based lighting the distance setting is a setting where the light's influence fades out linearly. Effectively
lightAffect = 1 - min(1, distanceFromLight / distance)
I don't know physically based lighting well but it seems to me real lights don't have a distance setting, they just have a power output (lumens) and decay based on the atmosphere density. Three.js has both a power setting and a decay setting although it's not clear at all what decay should be set to as the docs effectively just say to set it to 2.
What should I be setting distance for a physically based PointLight for example if I want physically based lighting?
'use strict';
/* global dat */
function main() {
const canvas = document.querySelector('#c');
const renderer = new THREE.WebGLRenderer({canvas: canvas});
renderer.physicallyCorrectLights = true;
const fov = 45;
const aspect = 2; // the canvas default
const zNear = 0.1;
const zFar = 100;
const camera = new THREE.PerspectiveCamera(fov, aspect, zNear, zFar);
camera.position.set(0, 10, 20);
camera.lookAt(0, 5, 0);
const scene = new THREE.Scene();
scene.background = new THREE.Color('black');
{
const planeSize = 40;
const planeGeo = new THREE.PlaneBufferGeometry(planeSize, planeSize);
const planeMat = new THREE.MeshPhongMaterial({
color: '#A86',
side: THREE.DoubleSide,
});
const mesh = new THREE.Mesh(planeGeo, planeMat);
mesh.rotation.x = Math.PI * -.5;
scene.add(mesh);
} {
const cubeSize = 4;
const cubeGeo = new THREE.BoxBufferGeometry(cubeSize, cubeSize, cubeSize);
const cubeMat = new THREE.MeshPhongMaterial({color: '#8AC'});
const mesh = new THREE.Mesh(cubeGeo, cubeMat);
mesh.position.set(cubeSize + 1, cubeSize / 2, 0);
scene.add(mesh);
}
{
const sphereRadius = 3;
const sphereWidthDivisions = 32;
const sphereHeightDivisions = 16;
const sphereGeo = new THREE.SphereBufferGeometry(sphereRadius, sphereWidthDivisions, sphereHeightDivisions);
const sphereMat = new THREE.MeshPhongMaterial({color: '#CA8'});
const mesh = new THREE.Mesh(sphereGeo, sphereMat);
mesh.position.set(-sphereRadius - 1, sphereRadius + 2, 0);
scene.add(mesh);
}
{
const color = 0xFFFFFF;
const intensity = 1;
const light = new THREE.PointLight(color, intensity);
light.power = 800;
light.distance = 20;
light.position.set(0, 10, 5);
scene.add(light);
light.decay = 2;
const helper = new THREE.PointLightHelper(light);
scene.add(helper);
const onChange = () => {
helper.update();
render();
};
setTimeout(onChange);
window.onresize = onChange;
const gui = new dat.GUI();
gui.add(light, 'distance', 0, 100).onChange(onChange);
gui.add(light, 'decay', 0, 4).onChange(onChange);
gui.add(light, 'power', 0, 3000).onChange(onChange);
}
function resizeRendererToDisplaySize(renderer) {
const canvas = renderer.domElement;
const width = canvas.clientWidth;
const height = canvas.clientHeight;
const needResize = canvas.width !== width || canvas.height !== height;
if (needResize) {
renderer.setSize(width, height, false);
}
return needResize;
}
function render() {
if (resizeRendererToDisplaySize(renderer)) {
const canvas = renderer.domElement;
camera.aspect = canvas.clientWidth / canvas.clientHeight;
camera.updateProjectionMatrix();
}
renderer.render(scene, camera);
}
}
main();
html, body {
margin: 0;
height: 100%;
}
#c {
width: 100%;
height: 100%;
display: block;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/96/three.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/dat-gui/0.7.2/dat.gui.min.js"></script>
<canvas id="c"></canvas>
Reading through three.js source and the paper it's linked to, at least as of r95 the distance setting should basically be Infinity for physically based lights.
In the paper they point out physically based lights shine to infinity but of course in a 3D engine that's no good. Most 3D engines need to compute the minimum number of lights per object drawn so a lightDistance setting was added, if the light is further way than lightDistance they can ignore the light. The problem is there will be sharp edge if they just stop using the light past lightDistance so they hacked in a falloff.
three.js copied that lightDistance and falloff setting from the paper but three.js does not cull lights from calculations when lights are far away so there seems to be no reason not to set distance to infinity AFAICT, at least as of r95.

Three.js, moving a partical along an EllipseCurve

I know questions related to my problem have been asked and answered before but three.js changed a lot in the last couple years and I'm having trouble finding what I need in the currently available examples.
I have an elliptical curve that I'd like to run particles along. My code runs without error but it doesn't actually move the particle anywhere. What am I missing?
var t = 0;
var curve = new THREE.EllipseCurve( .37, .15, .35, .25, 150, 450, false, 0 );
var points = curve.getPoints( 50 );
var curveGeometry = new THREE.BufferGeometry().setFromPoints( points );
var particleGeometry = new THREE.Geometry();
var particleMap = new THREE.TextureLoader().load( "/img/spark.png" );
var vertex = new THREE.Vector3();
vertex.x = points[0].x;
vertex.y = points[0].y;
vertex.z = 0;
particleGeometry.vertices.push(vertex);
particleMaterial = new THREE.PointsMaterial({
size: .05,
map: particleMap,
blending: THREE.AdditiveBlending,
depthTest: false,
transparent : true
});
particles = new THREE.Points( particleGeometry, particleMaterial );
scene.add(particles);
animate();
function animate() {
if (t <= 1) {
particles.position = curveGeometry.getPointAt(t)
t += 0.005
} else {
t = 0;
}
requestAnimationFrame( animate );
render();
}
function render() {
renderer.render( scene, camera );
}
Just a rough concept of how you can do it, using THREE.Geometry():
var scene = new THREE.Scene();
var camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 1, 1000);
camera.position.set(0, 0, 50);
var renderer = new THREE.WebGLRenderer({
antialias: true
});
renderer.setClearColor(0x404040);
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
var controls = new THREE.OrbitControls(camera, renderer.domElement);
var grid = new THREE.GridHelper(40, 40, "white", "gray");
grid.rotation.x = Math.PI * -0.5;
scene.add(grid);
var curve = new THREE.EllipseCurve(0, 0, 20, 20, 0, Math.PI * 2, false, 0);
// using of .getPoints(division) will give you a set of points of division + 1
// so, let's get the points manually :)
var count = 10;
var inc = 1 / count;
var pointAt = 0;
var points = [];
for (let i = 0; i < count; i++) {
let point = curve.getPoint(pointAt); // get a point of THREE.Vector2()
point.z = 0; // geometry needs points of x, y, z; so add z
point.pointAt = pointAt; // save position along the curve in a custom property
points.push(point);
pointAt += inc; // increment position along the curve for next point
}
var pointsGeom = new THREE.Geometry();
pointsGeom.vertices = points;
console.log(points);
var pointsObj = new THREE.Points(pointsGeom, new THREE.PointsMaterial({
size: 1,
color: "aqua"
}));
scene.add(pointsObj);
var clock = new THREE.Clock();
var time = 0;
render();
function render() {
requestAnimationFrame(render);
time = clock.getDelta();
points.forEach(p => {
p.pointAt = (p.pointAt + time * 0.1) % 1; // it always will be from 0 to 1
curve.getPoint(p.pointAt, p); //re-using of the current point
});
pointsGeom.verticesNeedUpdate = true;
renderer.render(scene, camera);
}
body {
overflow: hidden;
margin: 0;
}
<script src="https://threejs.org/build/three.min.js"></script>
<script src="https://threejs.org/examples/js/controls/OrbitControls.js"></script>

Three.js - Create new mesh from certain faces/vertices of another mesh

I´ve been several days struggling with a particular Three.js issue, and I cannot find any way to do it. This is my case:
1) I have a floating mesh, formed by several triangled faces. This mesh is created from the geometry returned by a loader, after obtaining its vertices and faces using getAttribute('position'): How to smooth mesh triangles in STL loaded BufferGeometry
2) What I want to do now is to "project" the bottom face agains the floor.
3) Later, with this new face added, create the resulting mesh of filling the space between the 3 vertices of both faces.
I already have troubles in step 2... To create a new face I´m supossed to have its 3 vertices already added to geometry.vertices. I did it, cloning the original face vertices. I use geometry.vertices.push() results to know their new indexes, and later I use that indexes (-1) to finally create the new face. But its shape is weird, also the positions and the size. I think I´m not getting the world/scene/vector position equivalence theory right :P
I tried applying this, with no luck:
How to get the absolute position of a vertex in three.js?
Converting World coordinates to Screen coordinates in Three.js using Projection
http://barkofthebyte.azurewebsites.net/post/2014/05/05/three-js-projecting-mouse-clicks-to-a-3d-scene-how-to-do-it-and-how-it-works
I discovered that if I directly clone the full original face and simply add it to the mesh, the face is added but in the same position, so I cannot then change its vertices to place it on the floor (or at least without modifying the original face vertices!). I mean, I can change their x, y, z properties, but they are in a very small measure that doesn´t match the original mesh dimensions.
Could someone help me get this concept right?
EDIT: source code
// Create geometry
var geo = new THREE.Geometry();
var geofaces = [];
var geovertices = [];
original_geometry.updateMatrixWorld();
for(var index in original_geometry.faces){
// Get original face vertexNormals to know its 3 vertices
var face = original_geometry[index];
var vertexNormals = face.vertexNormals;
// Create 3 new vertices, add it to the array and then create a new face using the vertices indexes
var vertexIndexes = [null, null, null];
for (var i = 0, l = vertexNormals.length; i < l; i++) {
var vectorClone = vertexNormals[i].clone();
vectorClone.applyMatrix4( original_geometry.matrixWorld );
//vectorClone.unproject(camera); // JUST TESTING
//vectorClone.normalize(); // JUST TESTING
var vector = new THREE.Vector3(vectorClone.x, vectorClone.z, vectorClone.y)
//vector.normalize(); // JUST TESTING
//vector.project(camera); // JUST TESTING
//vector.unproject(camera); // JUST TESTING
vertexIndexes[i] = geovertices.push( vector ) - 1;
}
var newFace = new THREE.Face3( vertexIndexes[0], vertexIndexes[1], vertexIndexes[2] );
geofaces.push(newFace);
}
// Assign filled arrays to the geometry
geo.faces = geofaces;
geo.vertices = geovertices;
geo.mergeVertices();
geo.computeVertexNormals();
geo.computeFaceNormals();
// Create a new mesh with resulting geometry and add it to scene (in this case, to the original mesh to keep the positions)
new_mesh = new THREE.Mesh( geo, new THREE.MeshFaceMaterial(material) ); // material is defined elsewhere
new_mesh.position.set(0, -100, 0);
original_mesh.add( new_mesh );
I created a fully operational JSFiddle with the case to try things and see the problem more clear. With this STL (smaller than my local example) I cannot even see the badly cloned faces added to the scene.. Maybe they are too small or out of focus.
Take a look to the calculateProjectedMesh() function, here is where I tried to clone and place the bottom faces (already detected because they have a different materialIndex):
JSFiddle: https://jsfiddle.net/tc39sgo1/
var container;
var stlPath = 'https://dl.dropboxusercontent.com/s/p1xp4lhy4wxmf19/Handle_Tab_floating.STL';
var camera, controls, scene, renderer, model;
var mouseX = 0,
mouseY = 0;
var test = true;
var meshPlane = null, meshStl = null, meshCube = null, meshHang = null;
var windowHalfX = window.innerWidth / 2;
var windowHalfY = window.innerHeight / 2;
/*THREE.FrontSide = 0;
THREE.BackSide = 1;
THREE.DoubleSide = 2;*/
var materials = [];
materials.push( new THREE.MeshPhongMaterial({color : 0x00FF00, side:0, shading: THREE.FlatShading, transparent: true, opacity: 0.9, overdraw : true, wireframe: false}) );
materials.push( new THREE.MeshPhongMaterial({color : 0xFF0000, transparent: true, opacity: 0.8, side:0, shading: THREE.FlatShading, overdraw : true, metal: false, wireframe: false}) );
materials.push( new THREE.MeshPhongMaterial({color : 0x0000FF, side:2, shading: THREE.FlatShading, overdraw : true, metal: false, wireframe: false}) );
var lineMaterial = new THREE.LineBasicMaterial({ color: 0x0000ff, transparent: true, opacity: 0.05 });
init();
animate();
function webglAvailable() {
try {
var canvas = document.createElement('canvas');
return !!(window.WebGLRenderingContext && (
canvas.getContext('webgl') || canvas.getContext('experimental-webgl')));
} catch (e) {
return false;
}
}
function init() {
container = document.createElement('div');
document.body.appendChild(container);
camera = new THREE.PerspectiveCamera(25, window.innerWidth / window.innerHeight, 0.1, 100000000);
camera.position.x = 1500;
camera.position.z = -2000;
camera.position.y = 1000;
controls = new THREE.OrbitControls(camera);
// scene
scene = new THREE.Scene();
var ambient = new THREE.AmbientLight(0x101030); //0x101030
scene.add(ambient);
var directionalLight = new THREE.DirectionalLight(0xffffff, 2);
directionalLight.position.set(0, 3, 0).normalize();
scene.add(directionalLight);
var directionalLight = new THREE.DirectionalLight(0xffffff, 2);
directionalLight.position.set(0, 1, -2).normalize();
scene.add(directionalLight);
if (webglAvailable()) {
renderer = new THREE.WebGLRenderer();
} else {
renderer = new THREE.CanvasRenderer();
}
renderer.setClearColor( 0xCDCDCD, 1 );
// renderer = new THREE.WebGLRenderer();
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
container.appendChild(renderer.domElement);
document.addEventListener('mousemove', onDocumentMouseMove, false);
window.addEventListener('resize', onWindowResize, false);
createPlane(500, 500);
createCube(500);
loadStl();
}
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}
function onDocumentMouseMove(event) {
mouseX = (event.clientX - windowHalfX) / 2;
mouseY = (event.clientY - windowHalfY) / 2;
}
function animate() {
requestAnimationFrame(animate);
render();
}
function render() {
renderer.render(scene, camera);
}
function createPlane(width, height) {
var planegeometry = new THREE.PlaneBufferGeometry(width, height, 0, 0);
var material = new THREE.MeshLambertMaterial({
color: 0xFFFFFF,
side: THREE.DoubleSide
});
planegeometry.computeBoundingBox();
planegeometry.center();
meshPlane = new THREE.Mesh(planegeometry, material);
meshPlane.rotation.x = 90 * (Math.PI/180);
//meshPlane.position.y = -height/2;
scene.add(meshPlane);
}
function createCube(size) {
var geometry = new THREE.BoxGeometry( size, size, size );
geometry.computeFaceNormals();
geometry.mergeVertices();
geometry.computeVertexNormals();
geometry.center();
var material = new THREE.MeshPhongMaterial({
color: 0xFF0000,
opacity: 0.04,
transparent: true,
wireframe: true,
side: THREE.DoubleSide
});
meshCube = new THREE.Mesh(geometry, material);
meshCube.position.y = size/2;
scene.add(meshCube);
}
function loadStl() {
var loader = new THREE.STLLoader();
loader.load( stlPath, function ( geometry ) {
// Convert BufferGeometry to Geometry
var geometry = new THREE.Geometry().fromBufferGeometry( geometry );
geometry.computeBoundingBox();
geometry.computeVertexNormals();
geometry.center();
var faces = geometry.faces;
for(var index in faces){
var face = faces[index];
var faceNormal = face.normal;
var axis = new THREE.Vector3(0,-1,0);
var angle = Math.acos(axis.dot(faceNormal));
var angleReal = (angle / (Math.PI/180));
if(angleReal <= 70){
face.materialIndex = 1;
}
else{
face.materialIndex = 0;
}
}
geometry.computeFaceNormals();
geometry.computeVertexNormals();
meshStl = new THREE.Mesh(geometry, new THREE.MeshFaceMaterial(materials));
meshStl.position.x = 0;
meshStl.position.y = 400;
scene.add( meshStl );
// Once loaded, calculate projections mesh
calculateProjectedMesh();
});
}
function calculateProjectedMesh(){
var geometry = meshStl.geometry;
var faces = geometry.faces;
var vertices = geometry.vertices;
var geometry_projected = new THREE.Geometry();
var faces_projected = [];
var vertices_projected = [];
meshStl.updateMatrixWorld();
for(var index in faces){
var face = faces[index];
// This are the faces
if(face.materialIndex == 1){
var vertexIndexes = [face.a, face.b, face.c];
for (var i = 0, l = vertexIndexes.length; i < l; i++) {
var relatedVertice = vertices[ vertexIndexes[i] ];
var vectorClone = relatedVertice.clone();
console.warn(vectorClone);
vectorClone.applyMatrix4( meshStl.matrixWorld );
////////////////////////////////////////////////////////////////
// TEST: draw line
var geometry = new THREE.Geometry();
geometry.vertices.push(new THREE.Vector3(vectorClone.x, vectorClone.y, vectorClone.z));
//geometry.vertices.push(new THREE.Vector3(vectorClone.x, vectorClone.y, vectorClone.z));
geometry.vertices.push(new THREE.Vector3(vectorClone.x, meshPlane.position.y, vectorClone.z));
var line = new THREE.Line(geometry, lineMaterial);
scene.add(line);
console.log("line added");
////////////////////////////////////////////////////////////////
vectorClone.y = 0;
var vector = new THREE.Vector3(vectorClone.x, vectorClone.y, vectorClone.z);
vertexIndexes[i] = vertices_projected.push( vector ) - 1;
}
var newFace = new THREE.Face3( vertexIndexes[0], vertexIndexes[1], vertexIndexes[2] );
newFace.materialIndex = 2;
faces_projected.push(newFace);
}
}
geometry_projected.faces = faces_projected;
geometry_projected.vertices = vertices_projected;
geometry_projected.mergeVertices();
console.info(geometry_projected);
meshHang = new THREE.Mesh(geometry_projected, new THREE.MeshFaceMaterial(materials));
var newY = -(2 * meshStl.position.y) + 0;
var newY = -meshStl.position.y;
meshHang.position.set(0, newY, 0);
meshStl.add( meshHang );
}
EDIT: Finally!! I got it! To clone the original faces I must access their 3 original vertices using "a", "b" and "c" properties, which are indexes referencing Vector3 instances in the "vertices" array of the original geometry.
I cloned the 3 vertices flatting the Z position to zero, use their new indexes to create the new face and add it to the projection mesh (in blue).
I´m also adding lines as a visual union between both faces. Now I´m ready for step 3, but I think this is complex enough to close this question.
Thanks for the updateMatrixWorld clue! It was vital to achieve my goal ;)
try this
original_geometry.updateMatrixWorld();
var vertexIndexes = [null, null, null];
for (var i = 0, l = vertexNormals.length; i < l; i++) {
var position = original_geometry.geometry.vertices[i].clone();
position.applyMatrix4( original_geometry.matrixWorld );
var vector = new THREE.Vector3(position.x, position.y, position.z)
vertexIndexes[i] = geovertices.push( vector ) - 1;
}

Resources