How to create sphere based pie chart with three.js? - three.js

Is there a way to create similar mesh in three.js?
Basically this is 3D pie chart clipped by half-sphere.
The only idea I have is to use clipping planes. Is there more simple approach?

It really depends on exactly what you want but the SphereBufferGeometry has parameters to create less than the entire sphere. The 4th and 5th parameters set the start and length around the width. The 6th and 7th set the start and length around the height
<canvas id="c"></canvas>
<script type="module">
import * as THREE from 'https://threejsfundamentals.org/threejs/resources/threejs/r113/build/three.module.js';
function main() {
const canvas = document.querySelector('#c');
const renderer = new THREE.WebGLRenderer({canvas});
const scene = new THREE.Scene();
scene.background = new THREE.Color('white');
const fov = 75;
const aspect = 2; // the canvas default
const near = 0.1;
const far = 5;
const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
camera.position.set(0, 2, 1.5);
camera.lookAt(0, 0, 0);
const cameraStick = new THREE.Object3D();
scene.add(cameraStick);
cameraStick.add(camera);
function addLight(...pos) {
const color = 0xFFFFFF;
const intensity = 1;
const light = new THREE.DirectionalLight(color, intensity);
light.position.set(...pos);
scene.add(light);
}
addLight(-1, 2, 4);
addLight( 1, 2, -4);
const amounts = [
123,
47,
];
const total = amounts.reduce((a, b) => a + b);
amounts.reduce((amountSoFar, amount, ndx) => {
const geo = new THREE.SphereBufferGeometry(
1, // radius
32, // width segments
16, // height segments
amountSoFar / total * Math.PI * 2, // phiStart
amount / total * Math.PI * 2, // phiLength
0, // thetaStart
Math.PI / 2, // thetaLength
);
const mat = new THREE.MeshPhongMaterial();
mat.color.setHSL(amountSoFar / total, 1, 0.5);
scene.add(new THREE.Mesh(geo, mat));
return amountSoFar + amount;
}, 0);
function render(time) {
time *= 0.001;
cameraStick.rotation.y = time;
renderer.render(scene, camera);
requestAnimationFrame(render);
}
requestAnimationFrame(render);
}
main();
</script>
If you want to seal the bottom and the sides you have several options.
Write your own sphere generation code. Just copy the source from three.js and add code to add the faces to seal the sides
Use CircleBufferGeometry that has similar parameters to make the sides and ends. It's up to you if you want to merge those into a single geometry.
Note that trying to draw a chart like that with transparency will have issues.
<canvas id="c"></canvas>
<script type="module">
import * as THREE from 'https://threejsfundamentals.org/threejs/resources/threejs/r113/build/three.module.js';
function main() {
const canvas = document.querySelector('#c');
const renderer = new THREE.WebGLRenderer({canvas});
const scene = new THREE.Scene();
scene.background = new THREE.Color('white');
const fov = 75;
const aspect = 2; // the canvas default
const near = 0.1;
const far = 5;
const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
camera.position.set(0, 2, 1.5);
camera.lookAt(0, 0, 0);
const cameraStick = new THREE.Object3D();
scene.add(cameraStick);
cameraStick.add(camera);
function addLight(...pos) {
const color = 0xFFFFFF;
const intensity = 1;
const light = new THREE.DirectionalLight(color, intensity);
light.position.set(...pos);
scene.add(light);
}
addLight(-1, 2, 4);
addLight( 1, 2, -4);
const amounts = [
120,
35,
];
const total = amounts.reduce((a, b) => a + b);
amounts.reduce((amountSoFar, amount, ndx) => {
const arc = amount / total * Math.PI * 2;
const base = new THREE.Object3D();
scene.add(base);
base.rotation.y = (amountSoFar / total) * Math.PI * 2 + arc / 2;
const offset = new THREE.Object3D();
offset.position.x = -0.05;
base.add(offset);
const geo = new THREE.SphereBufferGeometry(
1, // radius
32, // width segments
16, // height segments
-arc / 2, // phiStart;
arc, // phiLength,
0, // thetaStart
Math.PI / 2, // thetaLength
);
const mat = new THREE.MeshPhongMaterial();
mat.color.setHSL(amountSoFar / total, 1, 0.5);
const wedge = new THREE.Mesh(geo, mat);
offset.add(wedge);
const end = new THREE.CircleBufferGeometry(
1, // radius
16, // segments
Math.PI / 2, // thetaStart
Math.PI / 2, // thetaLength
);
const endMesh = new THREE.Mesh(end, mat);
endMesh.rotation.y = arc / 2;
offset.add(endMesh);
const end2 = new THREE.CircleBufferGeometry(
1, // radius
16, // segments
0, // thetaStart
Math.PI / 2, // thetaLength
);
const endMesh2 = new THREE.Mesh(end2, mat);
endMesh2.rotation.y = Math.PI - arc / 2;
offset.add(endMesh2);
const bottom = new THREE.CircleBufferGeometry(
1, // radius
32, // segments
Math.PI + -arc / 2, // thetaStart
arc, // thetaLength
);
const bottomMesh = new THREE.Mesh(bottom, mat);
bottomMesh.rotation.set(Math.PI / 2, 0, 0);
offset.add(bottomMesh);
return amountSoFar + amount;
}, 0);
function render(time) {
time *= 0.001;
cameraStick.rotation.y = time;
renderer.render(scene, camera);
requestAnimationFrame(render);
}
requestAnimationFrame(render);
}
main();
</script>

Related

ThreeJS raycasting problem with an Orthographic camera in isometric-like view

I'm pulling my hair off this one. I have to work with a specific camera angle for a 3D projet with the constraint of using an Orthographic Camera. I need to be able to precisely click on the floor for gameplay purposes. The ThreeJS Raycast doesn't seem to work properly (or maybe I set something the wrong way?). In a top-down view like angle, it works better.
Here is a fiddle that explains the kind of situation I'm in: https://jsfiddle.net/p6td5oak/42/
const sceneWidth = window.innerWidth;
const sceneHeight = window.innerHeight;
const scene = new THREE.Scene();
const camera = new THREE.OrthographicCamera( -sceneWidth / 2, sceneWidth / 2, sceneHeight / 2, -sceneHeight / 2, -1000, 1000 );
camera.rotation.set(
-Math.PI / 12,
Math.PI / 12,
Math.PI / 24
);
camera.position.set(0, 1, 0);
camera.zoom = 2;
camera.updateProjectionMatrix();
const renderer = new THREE.WebGLRenderer();
renderer.setSize( sceneWidth, sceneHeight );
document.body.appendChild( renderer.domElement );
const whiteMaterial = new THREE.MeshBasicMaterial({});
const redMaterial = new THREE.MeshBasicMaterial({
color: 0xFF0000
});
const size = 100;
const geometry = new THREE.PlaneGeometry(size, size, 10, 10);
for (var x = 0; x < 2; x++)
{
for (var z = 0; z < 2; z++)
{
let mesh = new THREE.Mesh(geometry, ((x + z) % 2 ? whiteMaterial : redMaterial));
mesh.rotation.set(
-Math.PI / 2,
0,
0
);
mesh.position.set(
x*size,
0,
z*size
)
scene.add(mesh);
}
}
var raycaster = new THREE.Raycaster();
window.addEventListener("pointerup", function(e)
{
var screenPos = new THREE.Vector2();
screenPos.x = (e.clientX / window.innerWidth) * 2 - 1;
screenPos.y = - (e.clientY / window.innerHeight) * 2 + 1;
raycaster.setFromCamera(screenPos, camera);
var rays = raycaster.intersectObjects(scene.children, true);
for (var i = 0; i < rays.length; i++)
{
scene.remove(rays[i].object);
}
}
.bind(this));
function animate() {
requestAnimationFrame( animate );
renderer.render( scene, camera );
}
animate();
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
In the example, I try to remove the planes as soon as they are clicked. As you can see, the top two planes can be removed if you click around their top-left corner. The other twos cannot even be triggered.
If someone have an idea what's going on, you'll be my hero.
Thanks!
PS: I have basic knowledge of ThreeJS but I'm far from being expert
Raycaster only detects objects in front of the camera, and your camera is located near the origin. Move the camera back.
Also, the near value of your orthographic camera is invalid. From the documentation:
The valid range is between 0 and the current value of the far plane.
Negative values are not supported.
const sceneWidth = window.innerWidth;
const sceneHeight = window.innerHeight;
const scene = new THREE.Scene();
const camera = new THREE.OrthographicCamera(-sceneWidth / 2, sceneWidth / 2, sceneHeight / 2, -sceneHeight / 2, 0.1, 1000);
camera.rotation.set(
-Math.PI / 12,
Math.PI / 12,
Math.PI / 24
);
camera.position.set(100, 100, 500);
camera.zoom = 2;
camera.updateProjectionMatrix();
const renderer = new THREE.WebGLRenderer({
antialias: true
});
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(sceneWidth, sceneHeight);
document.body.appendChild(renderer.domElement);
const whiteMaterial = new THREE.MeshBasicMaterial();
const redMaterial = new THREE.MeshBasicMaterial({
color: 0xFF0000
});
const size = 100;
const geometry = new THREE.PlaneGeometry(size, size, 10, 10);
for (let x = 0; x < 2; x++) {
for (let z = 0; z < 2; z++) {
let mesh = new THREE.Mesh(geometry, ((x + z) % 2 ? whiteMaterial : redMaterial));
mesh.rotation.set(
-Math.PI / 2,
0,
0
);
mesh.position.set(
x * size,
0,
z * size
)
scene.add(mesh);
}
}
const raycaster = new THREE.Raycaster();
const screenPos = new THREE.Vector2();
renderer.domElement.addEventListener("pointerup", function(e) {
screenPos.x = (e.clientX / window.innerWidth) * 2 - 1;
screenPos.y = -(e.clientY / window.innerHeight) * 2 + 1;
raycaster.setFromCamera(screenPos, camera);
const intersections = raycaster.intersectObject(scene, true);
for (let i = 0; i < intersections.length; i++) {
scene.remove(intersections[i].object);
}
});
function animate() {
requestAnimationFrame(animate);
renderer.render(scene, camera);
}
animate();
body {
margin: 0;
}
<script src="https://cdn.jsdelivr.net/npm/three#0.129/build/three.js"></script>

Piecing together meshes in ThreeJS causes visible seam

I'm trying to piece together a sphere with individual slices. Basically, I have multiple SphereGeoemtery slices that form a sphere and used to project a panorama. Slices are used for lazy loading very large panoramas.
With the default texture wrapping mode (THREE.ClampToEdgeWrapping) on these slices, from far away the panorama looks fine but if you zoom in it's very clear the edges of the meshes are stretching, causing visible seams. It make sense since it's stretching the last pixel at the edge..
I also tried changing wrapping mode to THREE.RepeatWrapping, however, the seam becomes completely visible:
So my question is, what's the best method here for piecing together meshes? Or is this just unavoidable?
Off the top of my head you'd have to make each texture contain one border row and border column in each direction that's a repeat of the its neighbor, then adjust the UV coordinates appropriately
For example if the big image is 8 pixels wide and 6 pixels tall
ABCDEFGH
IJKLMNOP
QRSTUVWX
YZ123456
789abcde
fghijklm
And you want to divide it into into 4 parts (each 4, 3)
then you'd need these 4 parts
ABCDE DEFGH
IJKLM LMNOP
QRSTU TUVWX
YZ123 23456
QRSTU TUVWX
YZ123 23456
789ab abcde
fghij ijklm
Also to make it easy repeat the edges so
AABCDE DEFGHH
AABCDE DEFGHH
IIJKLM LMNOPP
QQRSTU TUVWXX
YYZ123 234566
QQRSTU TUVWXX
YYZ123 234566
7789ab abcdee
ffghij ijklmm
ffghij ijklmm
Repeating the edges is because I'm assuming you're splitting into more than 2x2 so technically if you were going to split something 50 pixels wide into 5 parts you could do parts that are 11, 12, 12, 12, 11 in width. The edges being only 11 pixels instead of 12 would need a different UV adjustment. But, by repeating the edges we can make them all 12, 12, 12, 12, 12 so everything is consistant.
testing, left is normal split showing the seam. Right is the fixed one, no seam.
body {
margin: 0;
}
#c {
width: 100vw;
height: 100vh;
display: block;
}
<canvas id="c"></canvas>
<script type="module">
import * as THREE from 'https://threejsfundamentals.org/threejs/resources/threejs/r115/build/three.module.js';
function main() {
const canvas = document.querySelector('#c');
const renderer = new THREE.WebGLRenderer({canvas});
const fov = 75;
const aspect = 2; // the canvas default
const near = 0.1;
const far = 5;
const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
camera.position.z = 1;
const scene = new THREE.Scene();
// make our texture using a canvas to test
const bigImage = document.createElement('canvas');
{
const ctx = bigImage.getContext('2d');
const width = 32;
const height = 16;
ctx.canvas.width = width;
ctx.canvas.height = height;
const gradient = ctx.createLinearGradient(0, 0, width, height);
gradient.addColorStop(0, 'red');
gradient.addColorStop(0.5, 'yellow');
gradient.addColorStop(1, 'blue');
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, width, height);
}
const forceTextureInitialization = function() {
const material = new THREE.MeshBasicMaterial();
const geometry = new THREE.PlaneBufferGeometry();
const scene = new THREE.Scene();
scene.add(new THREE.Mesh(geometry, material));
const camera = new THREE.Camera();
return function forceTextureInitialization(texture) {
material.map = texture;
renderer.render(scene, camera);
};
}();
// bad
{
const ctx = document.createElement('canvas').getContext('2d');
// split the texture into 4 parts across 4 planes
const across = 2;
const down = 2;
const pixelsAcross = bigImage.width / across;
const pixelsDown = bigImage.height / down;
ctx.canvas.width = pixelsAcross;
ctx.canvas.height = pixelsDown;
for (let y = 0; y < down; ++y) {
for (let x = 0; x < across; ++x) {
ctx.clearRect(0, 0, pixelsAcross, pixelsDown);
ctx.drawImage(bigImage,
x * pixelsAcross, (down - 1 - y) * pixelsDown, pixelsAcross, pixelsDown,
0, 0, pixelsAcross, pixelsDown);
const texture = new THREE.CanvasTexture(ctx.canvas);
// see https://threejsfundamentals.org/threejs/lessons/threejs-canvas-textures.html
forceTextureInitialization(texture);
const geometry = new THREE.PlaneBufferGeometry(1 / across, 1 / down);
const material = new THREE.MeshBasicMaterial({map: texture});
const plane = new THREE.Mesh(geometry, material);
scene.add(plane);
plane.position.set(-1 + x / across, y / down - 0.25, 0);
}
}
}
// good
{
const ctx = document.createElement('canvas').getContext('2d');
// split the texture into 4 parts across 4 planes
const across = 2;
const down = 2;
const pixelsAcross = bigImage.width / across;
const pixelsDown = bigImage.height / down;
ctx.canvas.width = pixelsAcross + 2;
ctx.canvas.height = pixelsDown + 2;
// just draw the image at all these offsets.
// it would be more efficient to draw the edges
// 1 pixel wide but I'm lazy
const offsets = [
[ 0, 0],
[ 1, 0],
[ 2, 0],
[ 0, 1],
[ 2, 1],
[ 0, 2],
[ 1, 2],
[ 2, 2],
[ 1, 1],
];
for (let y = 0; y < down; ++y) {
for (let x = 0; x < across; ++x) {
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
let srcX = x * pixelsAcross - 1;
let srcY = (down - 1 - y) * pixelsDown - 1;
let dstX = 0;
let dstY = 0;
let width = pixelsAcross + 2;
let height = pixelsDown + 2;
ctx.drawImage(bigImage,
srcX, srcY, width, height,
dstX, dstY, width, height);
// handle edges
if (srcX < 0) {
// repeat left edge
ctx.drawImage(bigImage,
0, srcY, 1, height,
0, dstY, 1, height);
}
if (srcY < 0) {
// repeat top edge
ctx.drawImage(bigImage,
srcX, 0, width, 1,
dstX, 0, width, 1);
}
if (srcX + width > bigImage.width) {
// repeat right edge
ctx.drawImage(bigImage,
bigImage.width - 1, srcY, 1, height,
ctx.canvas.width - 1, dstY, 1, height);
}
if (srcY + height > bigImage.height) {
// repeat bottom edge
ctx.drawImage(bigImage,
srcX, bigImage.height - 1, width, 1,
dstX, ctx.canvas.height - 1, width, 1);
}
// TODO: handle corners
const texture = new THREE.CanvasTexture(ctx.canvas);
texture.minFilter = THREE.LinearFilter;
// offset UV coords 1 pixel to skip the edge pixel
texture.offset.set(1 / ctx.canvas.width, 1 / ctx.canvas.height);
// only textureSize - 2 of the pixels in the texture
texture.repeat.set(pixelsAcross / ctx.canvas.width, pixelsDown / ctx.canvas.height);
// see https://threejsfundamentals.org/threejs/lessons/threejs-canvas-textures.html
forceTextureInitialization(texture);
const geometry = new THREE.PlaneBufferGeometry(1 / across, 1 / down);
const material = new THREE.MeshBasicMaterial({map: texture});
const plane = new THREE.Mesh(geometry, material);
scene.add(plane);
plane.position.set(1 + x / across - 0.5, y / down - 0.25, 0);
}
}
}
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(time) {
time *= 0.001;
if (resizeRendererToDisplaySize(renderer)) {
const canvas = renderer.domElement;
camera.aspect = canvas.clientWidth / canvas.clientHeight;
camera.updateProjectionMatrix();
}
renderer.render(scene, camera);
requestAnimationFrame(render);
}
requestAnimationFrame(render);
}
main();
</script>

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);

Three.js. Why does this RGBA texture not change the associated material opacity?

In three.js I create a material that uses a texture to control transparency. The texture is created from a canvas. The canvas is drawn with fillStyle of rgba. Alpha varies across the canvas. The effect I am after is to vary transparency across the object the material is attached to. That is not happening. The object remains opaque.
Code:
tubeTexture = new THREE.Texture(canvas);
tubeTexture.center.set(0.5, 0.5);
tubeTexture.rotation = Math.PI/2.0;
// turn off any filtering to create sharp edges when highlighting
// tube section based on colorRamp highlighting.
tubeTexture.minFilter = tubeTexture.magFilter = THREE.NearestFilter;
// let tubeMaterial = new THREE.MeshBasicMaterial({ map: tubeTexture });
let tubeMaterial = new THREE.MeshPhongMaterial({ map: tubeTexture });
tubeMaterial.side = THREE.DoubleSide;
tubeMaterial.transparent = true;
// let tubeMaterial = sceneManager.stickMaterial.clone();
const tubeMesh = new THREE.Mesh(tubeGeometry, tubeMaterial);
What am I missing?
It seems to work for me
'use strict';
/* global THREE */
function main() {
const canvas = document.querySelector('#c');
const renderer = new THREE.WebGLRenderer({
canvas: canvas
});
const fov = 75;
const aspect = 2; // the canvas default
const near = 0.1;
const far = 5;
const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
camera.position.z = 2;
const scene = new THREE.Scene();
scene.background = new THREE.Color('white');
{
const color = 0xFFFFFF;
const intensity = 1;
const light = new THREE.DirectionalLight(color, intensity);
light.position.set(-1, 2, 4);
scene.add(light);
}
const boxWidth = 1;
const boxHeight = 1;
const boxDepth = 1;
const geometry = new THREE.BoxGeometry(boxWidth, boxHeight, boxDepth);
const ctx = document.createElement('canvas').getContext('2d');
ctx.canvas.width = 256;
ctx.canvas.height = 256;
ctx.fillStyle = 'rgba(255, 255, 255, 0.25)';
ctx.beginPath();
ctx.arc(128, 128, 120, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = 'rgba(255, 255, 255, 0.5)';
ctx.beginPath();
ctx.arc(128, 128, 64, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = 'rgba(255, 255, 255, 1.0)';
ctx.beginPath();
ctx.arc(128, 128, 32, 0, Math.PI * 2);
ctx.fill();
const texture = new THREE.CanvasTexture(ctx.canvas);
const root = new THREE.Object3D();
scene.add(root);
function makeInstance(geometry, color, x) {
const material = new THREE.MeshPhongMaterial({
color,
map: texture,
transparent: true,
side: THREE.DoubleSide,
alphaTest: 0.1,
});
const cube = new THREE.Mesh(geometry, material);
root.add(cube);
cube.position.x = x;
return cube;
}
const cubes = [
makeInstance(geometry, 0x44aa88, 0),
makeInstance(geometry, 0x8844aa, -2),
makeInstance(geometry, 0xaa8844, 2),
];
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(time) {
time *= 0.001;
if (resizeRendererToDisplaySize(renderer)) {
const canvas = renderer.domElement;
camera.aspect = canvas.clientWidth / canvas.clientHeight;
camera.updateProjectionMatrix();
}
root.rotation.y = time * .2;
cubes.forEach((cube, ndx) => {
const speed = 1 + ndx * .1;
const rot = time * speed;
cube.rotation.x = rot;
cube.rotation.y = rot;
});
renderer.render(scene, camera);
requestAnimationFrame(render);
}
requestAnimationFrame(render);
}
main();
body {
margin: 0;
}
#c {
width: 100vw;
height: 100vh;
display: block;
}
<canvas id="c"></canvas>
<script src="https://threejsfundamentals.org/threejs/resources/threejs/r103/three.min.js"></script>
Of course there are the normal issues related to transparency and sorting. An object will not be consistantly transparent to itself, only to other objects.

Make a camera rotate along z axis while moving and changing lookAt (rollercoaster view) in Three.js

Hi i am having a problem maybe you can help me.
I have a camera that is going down a tube following a path. and a camera that rotates around that tube always pointing toward the next point in the tube. However, the camera sometimes can be below or beside the tube like a roller coaster. Like this
I have the position of point a and the position of the camera which is point b. I am always looking at point a+1
var bpoints = this.cameraPathpoints;
var apoints = this.pathPoints;
this.camera.position.copy(bpoints[i]);
this.camera.lookAt(apoints[i+1]);
The camera is always looking at the point correctly however i want that the camera rotates in its z axis so that it is always normal to the tube. I tried making some calculations so that the camera rotates in its z axis so that the camera always faces normal to the tube, however my calculations work only on certain positions. Maybe there is a simpler way to do this. Thank you very much for any help.
var angleRadians = Math.atan2(cpv[this.cameraPos].pos.y - centePoints[this.cameraPos].pos.y, cpv[this.cameraPos].pos.x - centePoints[this.cameraPos].pos.x);
if(angleRadians > 0 && angleRadians > Math.PI/2){
console.log("+90",(Math.PI/2) - angleRadians);
angleRadians = (Math.PI/2) - angleRadians;
this.camera.rotateZ(angleRadians);
console.log("rotated ", angleRadians * 180/Math.PI);
}
else if(angleRadians > 0 && angleRadians < Math.PI/2 && anglesum >
Math.PI/2){
console.log("-90",(Math.PI/2) - angleRadians);
angleRadians = (Math.PI/2) - angleRadians;
this.camera.rotateZ(-angleRadians);
console.log("rotated ", -angleRadians * 180/Math.PI);
}
else if(angleRadians > 0 && angleRadians < Math.PI/2){
console.log("-90",(Math.PI/2) + angleRadians);
angleRadians = -(Math.PI/2) - (angleRadians/Math.PI/2);
this.camera.rotateZ(angleRadians);
console.log("rotated ", angleRadians * 180/Math.PI);
}
else if(angleRadians < 0 && angleRadians < -Math.PI/2){
console.log("--90");
angleRadians = (Math.PI/2) + angleRadians;
this.camera.rotateZ(-angleRadians);
console.log("rotated ",-angleRadians * 180/Math.PI);
}else if(angleRadians < 0 && angleRadians > -Math.PI/2){
console.log("+-90");
angleRadians = (Math.PI/2) - angleRadians;
this.camera.rotateZ(-angleRadians);
console.log("rotated ", -angleRadians * 180/Math.PI);
}
Rather than doing math, make the camera a child of some other THREE.Object3D and use lookAt with that object. Set the camera's position and rotation relative to that object.
Below the object is called the mount. It goes down the path (center of the tube). The camera is a child of mount. The tube has a 1 unit radius so setting the camera.position.y to 1.5 makes it outside the tube. lookAt makes non-camera objects look down positive Z but the camera looks down negative Z so we rotate the camera 180 degrees.
Example:
'use strict';
/* global THREE */
function main() {
const canvas = document.querySelector('#c');
const renderer = new THREE.WebGLRenderer({canvas: canvas});
const scene = new THREE.Scene();
scene.background = new THREE.Color(0xAAAAAA);
const fov = 40;
const aspect = 2; // the canvas default
const near = 0.1;
const far = 1000;
const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
camera.position.y = 1.5; // 2 units above the mount
camera.rotation.y = Math.PI; // the mount will lootAt positiveZ
const mount = new THREE.Object3D();
mount.add(camera);
scene.add(mount);
{
const color = 0xFFFFFF;
const intensity = 1;
const light = new THREE.DirectionalLight(color, intensity);
light.position.set(-1, 2, 4);
scene.add(light);
}
{
const color = 0xFFFFFF;
const intensity = 1;
const light = new THREE.DirectionalLight(color, intensity);
light.position.set(1, -2, -4);
scene.add(light);
}
const curve = new THREE.Curves.GrannyKnot();
const tubularSegments = 200;
const radius = 1;
const radialSegments = 6;
const closed = true;
const tube = new THREE.TubeBufferGeometry(
curve, tubularSegments, radius, radialSegments, closed);
const texture = new THREE.DataTexture(new Uint8Array([128, 255, 255, 128]),
2, 2, THREE.LuminanceFormat);
texture.needsUpdate = true;
texture.magFilter = THREE.NearestFilter;
texture.wrapS = THREE.RepeatWrapping;
texture.wrapT = THREE.RepeatWrapping;
texture.repeat.set( 100, 4 );
const material = new THREE.MeshPhongMaterial({
map: texture,
color: '#8CF',
flatShading: true,
});
const mesh = new THREE.Mesh(tube, material);
scene.add(mesh);
const target = new THREE.Vector3();
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(time) {
time *= 0.001;
if (resizeRendererToDisplaySize(renderer)) {
const canvas = renderer.domElement;
camera.aspect = canvas.clientWidth / canvas.clientHeight;
camera.updateProjectionMatrix();
}
const t = time * 0.1 % 1;
curve.getPointAt(t, mount.position);
curve.getPointAt((t + 0.01) % 1, target);
mount.lookAt(target);
renderer.render(scene, camera);
requestAnimationFrame(render);
}
requestAnimationFrame(render);
}
main();
body { margin: 0; }
canvas { width: 100vw; height: 100vh; display: block; }
<canvas id="c"></canvas>
<script src="https://threejsfundamentals.org/threejs/resources/threejs/r102/three.min.js"></script>
<script src="https://threejsfundamentals.org/threejs/resources/threejs/r102/js/CurveExtras.js"></script>
You can easily orient the camera relative to the mount to say look more toward the path or way by setting camera.rotation.x. If you want to rotate around the mount either change the mount's up property or add another object between the mount and the camera and set its Z rotation.
'use strict';
/* global THREE */
function main() {
const canvas = document.querySelector('#c');
const renderer = new THREE.WebGLRenderer({canvas: canvas});
const scene = new THREE.Scene();
scene.background = new THREE.Color(0xAAAAAA);
const fov = 40;
const aspect = 2; // the canvas default
const near = 0.1;
const far = 1000;
const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
camera.position.y = 1.5; // 2 units above the mount
camera.rotation.y = Math.PI; // the mount will lootAt positiveZ
const mount = new THREE.Object3D();
const subMount = new THREE.Object3D();
subMount.rotation.z = Math.PI * .5;
subMount.add(camera);
mount.add(subMount);
scene.add(mount);
{
const color = 0xFFFFFF;
const intensity = 1;
const light = new THREE.DirectionalLight(color, intensity);
light.position.set(-1, 2, 4);
scene.add(light);
}
{
const color = 0xFFFFFF;
const intensity = 1;
const light = new THREE.DirectionalLight(color, intensity);
light.position.set(1, -2, -4);
scene.add(light);
}
const curve = new THREE.Curves.GrannyKnot();
const tubularSegments = 200;
const radius = 1;
const radialSegments = 6;
const closed = true;
const tube = new THREE.TubeBufferGeometry(
curve, tubularSegments, radius, radialSegments, closed);
const texture = new THREE.DataTexture(new Uint8Array([128, 255, 255, 128]),
2, 2, THREE.LuminanceFormat);
texture.needsUpdate = true;
texture.magFilter = THREE.NearestFilter;
texture.wrapS = THREE.RepeatWrapping;
texture.wrapT = THREE.RepeatWrapping;
texture.repeat.set( 100, 4 );
const material = new THREE.MeshPhongMaterial({
map: texture,
color: '#8CF',
flatShading: true,
});
const mesh = new THREE.Mesh(tube, material);
scene.add(mesh);
const target = new THREE.Vector3();
const target2 = new THREE.Vector3();
const mountToTarget = new THREE.Vector3();
const targetToTarget2 = new THREE.Vector3();
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(time) {
time *= 0.001;
if (resizeRendererToDisplaySize(renderer)) {
const canvas = renderer.domElement;
camera.aspect = canvas.clientWidth / canvas.clientHeight;
camera.updateProjectionMatrix();
}
const t = time * 0.1 % 1;
curve.getPointAt(t, mount.position);
curve.getPointAt((t + 0.01) % 1, target);
// set mount up to be perpenticular to the
// curve
curve.getPointAt((t + 0.02) % 1, target2);
mountToTarget.subVectors(mount.position, target).normalize();
targetToTarget2.subVectors(target2, target).normalize();
mount.up.crossVectors(mountToTarget, targetToTarget2);
mount.lookAt(target);
renderer.render(scene, camera);
requestAnimationFrame(render);
}
requestAnimationFrame(render);
}
main();
body { margin: 0; }
canvas { width: 100vw; height: 100vh; display: block; }
<canvas id="c"></canvas>
<script src="https://threejsfundamentals.org/threejs/resources/threejs/r102/three.min.js"></script>
<script src="https://threejsfundamentals.org/threejs/resources/threejs/r102/js/CurveExtras.js"></script>

Resources