see my previous question here for reference to what I am trying to achieve
TL;DR:
I am trying to get HTML elements to rotate in conjunction with OrbitControls to make it seem as if these HTML elements are stuck to the globe and move with it. (think map markers on a 3D earth above certain countries)
I achieved this almost successfully using the THREE.js CSS3DRenderer, and was able to get the HTML elements to stick to a location on my 3D globe with a lat/long calculation and rotate with the globe when OrbitControls are used.
The problem
The only issue I am having is that the HTML elements are scaling proportionate to how close/far they are from the camera. Usually I assume this would be the desired effect to help the sense of getting closer/further, but the scaling is causing me not to be able to size my HTML elements correctly and consistently, and also causing text and SVGs inside the elements to blur/become pixelated
What I want
I am looking for a way to turn off this scaling so that the HTML elements still transform in every other way, but stay the same size (i.e. scale(1, 1, 1) or their original scale) no matter where they are in the 3D renderer created by CSS3DRenderer.
I assume I will have to edit the CSS3DRenderer.js code for this, but I have absolutely no idea where to start and I cannot find any information anywhere else.
Thanks.
Some of my code:
Creating the CSS3DRenderer
//CSS3D Renderer
rendererHTML = new THREE.CSS3DRenderer();
rendererHTML.setSize(WIDTH, HEIGHT);
rendererHTML.domElement.classList.add('CSS3D-container');
containerHTML = document.querySelector('.globe__container');
containerHTML.appendChild(rendererHTML.domElement);
Resizing function (called on window resize event)
HEIGHT = sizeControlElem.getBoundingClientRect().width;
WIDTH = sizeControlElem.getBoundingClientRect().width;
renderer.setSize(WIDTH, HEIGHT);
rendererHTML.setSize(WIDTH, HEIGHT);
camera.aspect = WIDTH / HEIGHT;
camera.updateProjectionMatrix();
Creating the CSS3DSprite objects from <li> elements in the HTML and setting their initial positions on the globe
for (let key in this.locationsObject) {
_this.locationsObject[key].coordinates = calcPosFromLatLonRad(this.locationsObject[key].lat, this.locationsObject[key].long, 300);
let CSS3D_Object = new THREE.CSS3DSprite(_this.locationsObject[key].element);
CSS3D_Object.position.set(_this.locationsObject[key].coordinates[0], _this.locationsObject[key].coordinates[1], _this.locationsObject[key].coordinates[2]);
CSS3D_Object.receiveShadow = false;
CSS3D_Object.castShadow = false;
sceneHTML.add(CSS3D_Object);
_this.locationsObject[key].CSS_Object = CSS3D_Object;
console.info(CSS3D_Object);
}
You can see some more of my code in the question here
The only way to stop the scaling is by projecting the 3D positions to 2D with the Vector3.project() method. Take a look at the code sample below, I commented the key points in the JavaScript code, but a quick explanation is this:
Copy the 3D position where you want the hotspot into a new vector.
Use vector.project(camera) to translate that 3D point to 2D coordinates.
Transform the range of 2D coords from [-1, 1] to [0, window.width]
Apply these coordinates via CSS to your hotspot.
Bonus: You can still use the .z attribute of the 2D vector to determine if it's within the camera's frustum or not.
var camera, controls, scene, renderer;
// This array will hold all positions in 3D space
var posArray3D = [];
// This array will hold all hotspot DIVs
var divArray = [];
// Create temp vector to reuse on loops
var tempVec = new THREE.Vector3();
init();
animate();
function init() {
scene = new THREE.Scene();
scene.background = new THREE.Color( 0xcccccc );
renderer = new THREE.WebGLRenderer( { antialias: true } );
renderer.setPixelRatio( window.devicePixelRatio );
renderer.setSize( window.innerWidth, window.innerHeight );
document.body.appendChild( renderer.domElement );
camera = new THREE.PerspectiveCamera( 60, window.innerWidth / window.innerHeight, 1, 3000 );
camera.position.set( 400, 200, 0 );
// controls
controls = new THREE.OrbitControls( camera, renderer.domElement );
controls.enableDamping = true;
controls.dampingFactor = 0.25;
controls.screenSpacePanning = false;
controls.minDistance = 100;
controls.maxDistance = 500;
controls.maxPolarAngle = Math.PI / 2;
// world
var geometry = new THREE.CylinderBufferGeometry( 0, 10, 30, 4, 1 );
var material = new THREE.MeshPhongMaterial( { color: 0xffffff, flatShading: true } );
// This is where all hotspot DIVs will go
var hotspotBox = document.getElementById("hotspotBox");
for ( var i = 0; i < 100; i ++ ) {
var mesh = new THREE.Mesh( geometry, material );
mesh.position.x = Math.random() * 1600 - 800;
mesh.position.y = 0;
mesh.position.z = Math.random() * 1600 - 800;
mesh.updateMatrix();
mesh.matrixAutoUpdate = false;
scene.add( mesh );
// Populate array of 3D positions
posArray3D.push(mesh.position);
// Create 'hotspot' DIV, and place within 'hotspotBox' holder
divArray.push(document.createElement("div"));
divArray[i].classList.add("hotspot");
hotspotBox.appendChild(divArray[i]);
}
// lights
var light = new THREE.DirectionalLight( 0xffffff );
light.position.set( 1, 1, 1 );
scene.add( light );
var light = new THREE.DirectionalLight( 0x002288 );
light.position.set( - 1, - 1, - 1 );
scene.add( light );
var light = new THREE.AmbientLight( 0x222222 );
scene.add( light );
window.addEventListener( 'resize', onWindowResize, false );
}
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize( window.innerWidth, window.innerHeight );
}
// Loops through all divs and updates their positions based on the camera
function updateDivs() {
var vectorScreen = new THREE.Vector3();
// Loop through all positions
for (var i = 0; i < posArray3D.length; i ++) {
vectorScreen.copy(worldToScreen(posArray3D[i], camera));
// Update CSS attributes of each DIV
divArray[i].style.transform = "translate(" + vectorScreen.x + "px, " + vectorScreen.y + "px)";
// Checks for depth, hides if it's behind the camera
if(vectorScreen.z <= 1) {
divArray[i].style.display = "block";
} else {
divArray[i].style.display = "none";
}
}
}
// Projects 3D coordinates into 2D space
function worldToScreen(_position, _cam) {
tempVec.copy(_position);
tempVec.project(_cam);
// Converts range from [-1, 1] to [0, windowWidth]
tempVec.x = ( tempVec.x + 1 ) * window.innerWidth / 2;
tempVec.y = ( - tempVec.y + 1 ) * window.innerHeight / 2;
return tempVec;
}
function animate() {
requestAnimationFrame( animate );
controls.update();
updateDivs();
render();
}
function render() {
renderer.render( scene, camera );
}
body {
color: #000;
font-family:Monospace;
font-size:13px;
text-align:center;
font-weight: bold;
background-color: #fff;
margin: 0px;
overflow: hidden;
}
/*hotspotBox holds all .hotspots It's placed on top of WebGL canvas*/
#hotspotBox{
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
border: 1px dashed #f90;
pointer-events: none;
}
/*100 hotspots to be placed within #hotspotBox */
.hotspot {
background: #f90;
width: 10px;
height: 10px;
border-radius: 5px;
position: absolute;
cursor: pointer;
pointer-events: auto;
}
<div id="hotspotBox"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/99/three.js"></script>
<script src="https://cdn.rawgit.com/mrdoob/three.js/master/examples/js/controls/OrbitControls.js"></script>
It turns out that the easiest solution to my question is simply to use the CSS2DRenderer instead of the CSS3DRenderer.
It essentially does the same thing but only transforms the HTML element with translate, and does not rotate or scale it, meaning you can modify the size of the HTML elements freely using CSS width and size etc... which is what I wanted to do.
It is implemented in the exact same way, the only thing I had to change in my code was replacing CSS3DSprite with CSS2DObject.
Read more about CSS2DRenderer here.
Related
I’ve asked in another forum but I thought I would like to be more clear on my problem.
Whats my intention?
Currently I am using three.js within WebView on my android device and created a scene which contains a simple box (which should be used as a bounding box) and a camera. My camera needs to be an interactive one with my android device, which means that I set the position by moving the device dynamically. These vectors are coming from a SLAM-algorithmn named Direct Sparse Odometry which recreates the camera position, I can also call these values with javascript by using the provided WebViewInterface from Android. My goal is to “walk around” the box dynamically without using the camera.lookAt()-Method every time I change the values, because if I move away from the box, the view should not be centered anymore (like an AR-Application), so the point of view should be created dynamically such as the position and rotation of the camera towards the object. My goal is to place an object over a real world object with three.js to scan it later with DSO by walking around the box to detect feature points. The whole visualisation should be created with three.js.
What is DSO?
DSO is a library to track the real environment by detecting points from a camera frames, which are provided by Android’s camera 2 API. This send me a 4x4 transformation Matrix with the current pose, which I try to apply on three.js’s camera position. Due to the complexity of this algorithm, lets pretend this gives me proper values (in meters, but I also tried to multiplicate the values by 10 or 100 to receive larger results than 0.XX).
Whats my Problem?
The box does not seem to have an absolute position, even if the values seems to be fixed. Every time when placing the Box, it seem to move in an opposite direction. After many adjustments on the dso values, I am crystal clear that this the problem is happening with three.js.
I’ve also tried to apply matrixes of the scene/camera and/or using the box as a child (because of the object-heredity), but the box seems not to have an absolute position inside the scene. Also I am not able to rotate the object that seem to be realistic.
Enclosed, you’ll find my code but p
lease note that I am using dynamically dummy values as a replacement for the dso values.
<body>
<canvas id="mCanvas">
</canvas>
</body>
<script>
// Var Init
var renderer, scene, camera, box, transformControl, orbitControl, geometry, material, poseMatrix;
var mPoints = [];
//Box coordinate
var xBCordinate, yBCordinate, zBCordinate, isScaled, posVec, startPosVec, lookPos, helper;
var process = false;
var scanActive = false;
var pointArr = [];
init();
animate();
function init() {
// renderer
renderer = new THREE.WebGLRenderer({canvas: document.getElementById("mCanvas"),
alpha: true});
renderer.setSize( window.innerWidth, window.innerHeight );
document.body.appendChild( renderer.domElement );
renderer.setClearColor(0xffffff, 0);
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
// scene
scene = new THREE.Scene();
// camera
camera = new THREE.PerspectiveCamera(
45,
window.innerWidth / window.innerHeight,
0.1,
1000
);
camera.up.set(0, 0, 1); // Definition of coordinationsystem
// set initial scale position of camera
camera.position.x = 0;
camera.position.y = -0.5;
camera.position.z = 0.15;
scene.add(camera);
// set position to look at
camera.lookAt(0,2.5,-0.2);
// apply values
camera.updateMatrix();
// light
var light = new THREE.HemisphereLight( 0xeeeeee, 0x888888, 1 );
light.position.set( 0, -0.75, 2.5 );
scene.add(light);
placeBox();
}
function placeBox()
{
geometry = new THREE.BoxGeometry(0.5, 1, 0.5); //3,5,3
material = new THREE.MeshLambertMaterial({color: 0xfece46});
box = new THREE.Mesh(geometry, material);
box.position.set(0, 2.5, -0.2);
box.updateMatrix();
scene.add(box);
}
function animate() {
requestAnimationFrame(animate);
if(process == false){
setCurrentPose();
}
renderer.render(scene, camera);
}
function setCurrentPose(){
process = true;
// this is where I receive the position data via Android
// but lets try using random numbers between 0.01 - 0.99 (which are the results interval of dso)
moveRotateCamera();
}
function moveRotateCamera(){
// Create Vector to work with
posVec = new THREE.Vector3();
posVec.x = getRandomFloat(0.01, 0.99);
posVec.y = pgetRandomFloat(0.01, 0.99);
posVec.z = getRandomFloat(0.01, 0.99);
camera.position.x = posVec.x;
camera.position.y = (posVec.y) - 0.50; // minus initial scale position
camera.position.z = (posVec.z) + 0.15;
// camera.updateMatrix(); <- seem to change nothing such as UpdateWorldMatrix() etc.
// camera rotation tried to calculate with quaternions (result NaN) and/or euler by using former and current point.
process = false;
}
function getRandomFloat(min, max) {
return Math.random() * (max - min) + min;
}
// My attempts in trying to calculate the rotation
/*
function setQuaternionRotation(poseMatrix){
// TODO: delete if not needed!
// adapted from http://www.euclideanspace.com/maths/geometry/rotations/conversions/matrixToQuaternion/index.htm, 2.12.2019, 2.34pm
mQuaternion = new THREE.Quaternion();
// Calculate Angle w
mQuaternion.w = ((Math.sqrt(Math.max(0, (1.0 + poseMatrix.elements[0] + poseMatrix.elements[5] + poseMatrix.elements[10])))/2.0));
//Sign x,y,z values of quaternion
mQuaternion.x = ((Math.sqrt(Math.max(0, (1.0 + poseMatrix.elements[0] - poseMatrix.elements[5] - poseMatrix.elements[10])))/2.0));
mQuaternion.y = ((Math.sqrt(Math.max(0, (1.0 - poseMatrix.elements[0] + poseMatrix.elements[5] - poseMatrix.elements[10])))/2.0));
mQuaternion.y = ((Math.sqrt(Math.max(0, (1.0 - poseMatrix.elements[0] - poseMatrix.elements[5] + poseMatrix.elements[10])))/2.0));
//Sign element values
mQuaternion.x = (Math.sign(mQuaternion.x * (poseMatrix.elements[6] - poseMatrix.elements[9])));
mQuaternion.y = (Math.sign(mQuaternion.y * (poseMatrix.elements[8] - poseMatrix.elements[2])));
mQuaternion.z = (Math.sign(mQuaternion.z * (poseMatrix.elements[1] - poseMatrix.elements[4])));
// debug
console.log("QuaternionVal: "+mQuaternion.x+ ", " +mQuaternion.y+", "+mQuaternion.z+", "+mQuaternion.w);
camera.applyQuaternion(mQuaternion);
camera.quaternion.normalize();
// debug
console.log("newCamRotation: "+camera.rotation.x +", "+camera.rotation.y+", "+ camera.rotation.z);
// camera.updateMatrix(true);
}
*/
</script>
Link to my Fiddle
Do you have any suggestions?
Thank you very much in advance!
Best regards,
FWIW. I think part of the issue is that the box is not centered about the camera rotation. I tweaked your fiddle by centering the box at the origin, in addition to using spherical coordinates to move the camera about. This keeps the camera at a uniform distance from the box, and with the box being centered about the rotation, it does not appear to be moving about the viewport...
<body>
<canvas id="mCanvas">
</canvas>
</body>
<script src="https://threejs.org/build/three.js"></script>
<script>
// Var Init
var renderer, scene, camera, box, transformControl, orbitControl, geometry, material, poseMatrix;
var mPoints = [];
//Box coordinate
var xBCordinate, yBCordinate, zBCordinate, isScaled, posVec, startPosVec, lookPos, helper;
var process = false;
var scanActive = false;
var pointArr = [];
var cameraSpherical;
init();
animate();
function init() {
// renderer
renderer = new THREE.WebGLRenderer({canvas: document.getElementById("mCanvas"),
alpha: true});
renderer.setSize( window.innerWidth, window.innerHeight );
document.body.appendChild( renderer.domElement );
renderer.setClearColor(0xffffff, 0);
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
// scene
scene = new THREE.Scene();
// camera
camera = new THREE.PerspectiveCamera(
45,
window.innerWidth / window.innerHeight,
0.1,
1000
);
camera.up.set(0, 0, 1); // Definition of coordinationsystem
// set initial scale position of camera
camera.position.x = 0;
camera.position.y = -0.5;
camera.position.z = 0.15;
scene.add(camera);
cameraSpherical = new THREE.Spherical( camera.position );
// set position to look at
camera.lookAt(0,2.5,-0.2);
// apply values
camera.updateMatrix();
// light
var light = new THREE.HemisphereLight( 0xeeeeee, 0x888888, 1 );
light.position.set( 0, -0.75, 2.5 );
scene.add(light);
placeBox();
}
function placeBox()
{
geometry = new THREE.BoxGeometry(0.5, 1, 0.5); //3,5,3
material = new THREE.MeshLambertMaterial({color: 0xfece46});
box = new THREE.Mesh(geometry, material);
box.position.set(0, 0, 0);
box.updateMatrix();
scene.add(box);
}
function animate() {
requestAnimationFrame(animate);
if(process == false){
setCurrentPose();
}
renderer.render(scene, camera);
}
function setCurrentPose(){
process = true;
// this is where I receive the position data via Android
// but lets try using random numbers between 0.01 - 0.99 (which are the results interval of dso)
moveRotateCamera();
}
function moveRotateCamera(){
// Create Vector to work with
/* posVec = new THREE.Vector3();
posVec.x = getRandomFloat(0.01, 0.05);
posVec.y = getRandomFloat(0.01, 0.05);
posVec.z = getRandomFloat(0.01, 0.02);
camera.position.x += posVec.x;
camera.position.y += posVec.y; // minus initial scale position
camera.position.z += posVec.z;
*/
cameraSpherical.radius = 5;
cameraSpherical.phi += getRandomFloat(0.001, 0.015);
cameraSpherical.theta += getRandomFloat(0.001, 0.015);
let xyz = new THREE.Vector3().setFromSpherical( cameraSpherical );
camera.position.x = xyz.x;
camera.position.y = xyz.y;
camera.position.z = xyz.z;
camera.lookAt(0,0,0);
camera.updateMatrix();
// camera.updateMatrix(); <- seem to change nothing such as UpdateWorldMatrix() etc.
// camera rotation tried to calculate with quaternions (result NaN) and/or euler by using former and current point.
process = false;
}
function getRandomFloat(min, max) {
return Math.random() * (max - min) + min;
}
// My attempts in trying to calculate the rotation
/*
function setQuaternionRotation(poseMatrix){
// TODO: delete if not needed!
// adapted from http://www.euclideanspace.com/maths/geometry/rotations/conversions/matrixToQuaternion/index.htm, 2.12.2019, 2.34pm
mQuaternion = new THREE.Quaternion();
// Calculate Angle w
mQuaternion.w = ((Math.sqrt(Math.max(0, (1.0 + poseMatrix.elements[0] + poseMatrix.elements[5] + poseMatrix.elements[10])))/2.0));
//Sign x,y,z values of quaternion
mQuaternion.x = ((Math.sqrt(Math.max(0, (1.0 + poseMatrix.elements[0] - poseMatrix.elements[5] - poseMatrix.elements[10])))/2.0));
mQuaternion.y = ((Math.sqrt(Math.max(0, (1.0 - poseMatrix.elements[0] + poseMatrix.elements[5] - poseMatrix.elements[10])))/2.0));
mQuaternion.y = ((Math.sqrt(Math.max(0, (1.0 - poseMatrix.elements[0] - poseMatrix.elements[5] + poseMatrix.elements[10])))/2.0));
//Sign element values
mQuaternion.x = (Math.sign(mQuaternion.x * (poseMatrix.elements[6] - poseMatrix.elements[9])));
mQuaternion.y = (Math.sign(mQuaternion.y * (poseMatrix.elements[8] - poseMatrix.elements[2])));
mQuaternion.z = (Math.sign(mQuaternion.z * (poseMatrix.elements[1] - poseMatrix.elements[4])));
// debug
console.log("QuaternionVal: "+mQuaternion.x+ ", " +mQuaternion.y+", "+mQuaternion.z+", "+mQuaternion.w);
camera.applyQuaternion(mQuaternion);
camera.quaternion.normalize();
// debug
console.log("newCamRotation: "+camera.rotation.x +", "+camera.rotation.y+", "+ camera.rotation.z);
// camera.updateMatrix(true);
}
*/
</script>
Not sure whether this helps you in the direction you're going, but hope it sheds some light.
I'm trying to engrave some text on a surface using Three.js. I've achieved it using csg.js and ThreeCSG and it works perfect, the result is really good but the problem is it takes a lot of time. On my PC it takes about 30 seconds to engrave the word Hello.
Searching for other solution I found this site. They make custom jewelry and you can engrave text on it and the time it takes to engrave the text is really short! So I assume they are not using csg.js. What other technique can be used to achieve this result?
I though about using bump maps, I should generate a bump map for each letter but I don't know if that's the correct approach.
I can see by looking at the shaders that the site your linked to uses bump maps.
I don't think you would create a bump map for each letter, you would just do all the drawing (text) on a single canvas and apply that as a bump map.
Click on "Run Code Snippet" below for a demo of canvas bump maps (click and drag in the white box).
I hope this helps.
var camera, scene, renderer, mesh, material, stats;
var drawStartPos = {x:0, y:0};
init();
setupCanvasDrawing();
animate();
function init() {
// Renderer.
renderer = new THREE.WebGLRenderer();
//renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
// Add renderer to page
document.getElementById('threejs-container').appendChild(renderer.domElement);
// Create camera.
camera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 1, 1000);
camera.position.z = 400;
// Create scene.
scene = new THREE.Scene();
// Create material
material = new THREE.MeshPhongMaterial();
// Create cube and add to scene.
var geometry = new THREE.BoxGeometry(200, 200, 200);
mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
// Create ambient light and add to scene.
var light = new THREE.AmbientLight(0x404040); // soft white light
scene.add(light);
// Create directional light and add to scene.
var directionalLight = new THREE.DirectionalLight(0xffffff);
directionalLight.position.set(1, 1, 1).normalize();
scene.add(directionalLight);
// Add listener for window resize.
window.addEventListener('resize', onWindowResize, false);
// Add stats to page.
stats = new Stats();
document.body.appendChild( stats.dom );
}
function setupCanvasDrawing() {
// get canvas and context
var drawingCanvas = document.getElementById('drawing-canvas');
var drawingContext = drawingCanvas.getContext('2d');
// draw white background
drawingContext.fillStyle = "#FFFFFF";
drawingContext.fillRect(0,0,128,128);
// set canvas as bumpmap
material.bumpMap = new THREE.Texture(drawingCanvas);
// set the variable to keep track of when to draw
var paint = false;
// add canvas event listeners
drawingCanvas.addEventListener('mousedown', function(e){
paint = true
drawStartPos = {x:e.offsetX, y:e.offsetY};
});
drawingCanvas.addEventListener('mousemove', function(e){
if(paint){
draw(drawingContext, e.offsetX, e.offsetY);
}
});
drawingCanvas.addEventListener('mouseup', function(e){
paint = false;
});
drawingCanvas.addEventListener('mouseleave', function(e){
paint = false;
});
}
// Draw function
function draw(drawContext, x, y) {
drawContext.moveTo(drawStartPos.x, drawStartPos.y);
drawContext.lineTo(x,y);
drawContext.stroke();
drawStartPos = {x:x, y:y};
material.bumpMap.needsUpdate = true;
}
function animate() {
requestAnimationFrame(animate);
mesh.rotation.x += 0.005;
mesh.rotation.y += 0.01;
renderer.render(scene, camera);
stats.update();
}
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}
body {
padding: 0;
margin: 0;
}
#drawing-canvas {
position: absolute;
background-color: #000;
top: 0px;
right: 0px;
z-index: 3;
}
#threejs-container {
position: absolute;
left: 0px;
top: 0px;
width: 100%;
height: 100%;
z-index: 1;
}
<script src="https://rawgit.com/mrdoob/three.js/r83/build/three.min.js"></script>
<script src="https://cdn.rawgit.com/mrdoob/stats.js/r17/build/stats.min.js"></script>
<canvas id="drawing-canvas" height="128" width="128"></canvas>
<div id="threejs-container"></div>
I'm working on a solar system in three.js and am curious if there is an easy way to make the labels for the planets I have below all show up the same size regardless of how far they are from the camera? I can't seem to find a solution to this. I figure you could calculate the distance from each label to the camera then come up with some sort of scaling factor based on that. Seems like there would be an easier way to accomplish this?
Thanks!
Updated with answer from prisoner849. Works excellent!
I figure you could calculate the distance from each label to the camera then come up with some sort of scaling factor based on that.
And it's very simple. Let's say, a THREE.Sprite() object (label) is a child of a THREE.Mesh() object (planet), then in your animation loop you need to do
var scaleVector = new THREE.Vector3();
var scaleFactor = 4;
var sprite = planet.children[0];
var scale = scaleVector.subVectors(planet.position, camera.position).length() / scaleFactor;
sprite.scale.set(scale, scale, 1);
I've made a very simple example of the Solar System, using this technique.
For the benefit of future visitors, the transform controls example does exactly this:
https://threejs.org/examples/misc_controls_transform.html
Here's how its done in the example code:
var factor;
if ( this.camera.isOrthographicCamera ) {
factor = ( this.camera.top - this.camera.bottom ) / this.camera.zoom;
} else {
factor = this.worldPosition.distanceTo( this.cameraPosition ) * Math.min( 1.9 * Math.tan( Math.PI * this.camera.fov / 360 ) / this.camera.zoom, 7 );
}
handle.scale.set( 1, 1, 1 ).multiplyScalar( factor * this.size / 7 );
Finally I found the answer to your question:
First, create a DOM Element:
<div class="element">Not Earth</div>
Then set CSS styles for it:
.element {position: absolute; top:0; left:0; color: white}
// |-------------------------------| |-----------|
// make the element on top of canvas is
// the canvas black, so text
// must be white
After that, create moveDom() function and run it every time you render the scene requestAnimationFrame()
geometry is the geometry of the mesh
cube is the mesh you want to create label
var moveDom = function(){
vector = geometry.vertices[0].clone();
vector.applyMatrix4(cube.matrix);
vector.project(camera);
vector.x = (vector.x * innerWidth/2) + innerWidth/2;
vector.y = -(vector.y * innerHeight/2) + innerHeight/2;
//Get the DOM element and apply transforms on it
document.querySelectorAll(".element")[0].style.webkitTransform = "translate("+vector.x+"px,"+vector.y+"px)";
document.querySelectorAll(".element")[0].style.transform = "translate("+vector.x+"px,"+vector.y+"px)";
};
You can create a for loop to set label for all the mesh in your scene.
Because this trick only set 2D position of DOM Element, the size of label is the same even if you zoom (the label is not part of three.js scene).
Full test case: https://jsfiddle.net/0L1rpayz/1/
var renderer, scene, camera, cube, vector, geometry;
var ww = window.innerWidth,
wh = window.innerHeight;
function init(){
renderer = new THREE.WebGLRenderer({canvas : document.getElementById('scene')});
renderer.setSize(ww,wh);
scene = new THREE.Scene();
camera = new THREE.PerspectiveCamera(50,ww/wh, 0.1, 10000 );
camera.position.set(0,0,500);
scene.add(camera);
light = new THREE.DirectionalLight(0xffffff, 1);
light.position.set( 0, 0, 500 );
scene.add(light);
//Vector use to get position of vertice
vector = new THREE.Vector3();
//Generate Not Earth
geometry = new THREE.BoxGeometry(50,50,50);
var material = new THREE.MeshLambertMaterial({color: 0x00ff00});
cube = new THREE.Mesh(geometry, material);
scene.add(cube);
//Render my scene
render();
}
var moveDom = function(){
vector = geometry.vertices[0].clone();
vector.applyMatrix4(cube.matrix);
vector.project(camera);
vector.x = (vector.x * ww/2) + ww/2;
vector.y = -(vector.y * wh/2) + wh/2;
//Get the DOM element and apply transforms on it
document.querySelectorAll(".element")[0].style.webkitTransform = "translate("+vector.x+"px,"+vector.y+"px)";
document.querySelectorAll(".element")[0].style.transform = "translate("+vector.x+"px,"+vector.y+"px)";
};
var counter = 0;
var render = function (a) {
requestAnimationFrame(render);
counter++;
//Move my cubes
cube.position.x = Math.cos((counter+1*150)/200)*(ww/6+1*80);
cube.position.y = Math.sin((counter+1*150)/200)*(70+1*80);
cube.rotation.x += .001*1+.002;
cube.rotation.y += .001*1+.02;
//Move my dom elements
moveDom();
renderer.render(scene, camera);
};
init();
body,html, canvas{width:100%;height:100%;padding:0;margin:0;overflow: hidden;}
.element{color:white;position:absolute;top:0;left:0}
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r79/three.min.js"></script>
<!-- My scene -->
<canvas id="scene"></canvas>
<div class="element">
<h1>Not Earth</h1>
</div>
If you downvote this, please tell me why. I will try my best to improve my posts.
If you are using spriteMaterial to present your text, you could try to set the sizeAttenuation attribute to false.
var spriteMaterial = new THREE.SpriteMaterial( { map: spriteMap, color: 0xffffff, sizeAttenuation:false } );
See more information from here:
https://threejs.org/docs/index.html#api/en/materials/SpriteMaterial.sizeAttenuation
So I'm working with Three.js and jQuery to create a small visual application. At the moment all I want is for all the meshes I have, to appear on screen.
The Problem: None of the meshes appear on screen whatsoever.
Exceptions: The renderer's clear color appears (0x00bfff) and console.log(scene) confirms that all the meshes are in the scene.
Attempts to Fix: Use THREE.Projector, THREE.Raycaster, change camera positioning, and many more attempts.
I'm still very new to Three.js and programming in general so please be very critical of my work. Anything helps! Thanks!
WORLD.JS
$(document).ready(function() {
initialize();
animate();
});
var initialize = function() {
clock = new THREE.Clock(); // timer used to calculate time between rendering frames
scene = new THREE.Scene(); // list of objects that are to be "read" (rendered)
camera = new THREE.PerspectiveCamera(35, // FOV
window.innerWidth / window.innerHeight, // Aspect Ratio
.1, // Near
10000); // Far
camera.position.set( 25, 25, 125 );
camera.lookAt( scene.position );
setupEnvironment();
setupAI();
renderer = new THREE.WebGLRenderer(); // renderer will draw as WebGL rather than HTML5 Canvas
renderer.setSize( window.innerWidth, window.innerHeight ); // size of the canvas that renderer will draw on
renderer.setClearColor( 0x00bfff, 1 );
document.body.appendChild( renderer.domElement ); // adds the canvas to the document
};
var animate = function() { // animates the scene with frames
requestAnimationFrame(animate); // works recursively
render(); // update and display
}
var render = function() {
var delta = clock.getDelta() // gets the seconds passed since the last call to this method
// AI collision needed
// AI update needed
renderer.render( scene, camera ) // repaint
}
var setupEnvironment = function() {
ground = new BoxMesh( 10, 0.1, 10, 0x6C4319, 1 );
positionThenAdd( ground, [[ 0, 0 ]] );
light1 = new THREE.PointLight( 0xFFFFFF, .5 );
light1.position.set( 10, 10, 10 );
scene.add( light1 );
light2 = new THREE.PointLight( 0xFFFFFF, 1 );
light2.position.set( -10, -10, 10 );
scene.add( light2 );
};
var setupAI = function() {
sheep = new BoxMesh( 1, 1, 1, 0xFFFFFF, 3 );
positionThenAdd( sheep, [[ 0, 0 ],
[ 4.5, 0 ],
[ 9.5, 0 ]]);
sheepHerder = new BoxMesh( 1, 1, 1, 0x996633, 1 );
positionThenAdd( sheepHerder, [[ 4.5, 7.5 ]] );
};
function BoxMesh( width, height, depth, hexColor, amount ) { // creates one or more box meshes
this.width = width;
this.height = height;
this.depth = depth;
this.hexColor = hexColor;
this.amount = amount; // amount of box meshes to be made
boxSize = new THREE.BoxGeometry( width, height, depth );
boxMaterial = new THREE.MeshLambertMaterial( { color: hexColor } );
var all = []; // will contain all of the box meshes
for(var n = 1; n <= amount; n++) { // adds a new box mesh to the end of the all array
all.push(new THREE.Mesh( boxSize, boxMaterial )); // uses the attributes given by the BoxMesh constructor's parameters
}
return all; // returns all of the created box meshes as an array;
}
var positionThenAdd = function( varMesh, posArrXByZ ) { // positions an object and then adds it to the scene
this.varMesh = varMesh; // variable name of the mesh(es) array
this.posArrXByZ = posArrXByZ; // posArrXByZ stands for "array of positions in the format of X-by-Z"
// posArrXByZ is a 2 dimensional array where the first dimension is for the specific mesh to be positioned...
// and the second dimension is the positional coordinates.
// posArrXByZ = [ [x0,z0], [x1,z1], ...[xn,zn] ]
for(var mesh = 0; mesh < varMesh.length; mesh++) { // mesh accesses the varMesh array
varMesh[mesh].position.set( varMesh[mesh].geometry.parameters.width/2 + posArrXByZ[mesh][0], // the x coordinate, varMesh[mesh].width/2 makes the x coordinate act upon the closest side
varMesh[mesh].geometry.parameters.height/2 + ground.height, // the y coordinate, which is pre-set to rest on top of the ground
varMesh[mesh].geometry.parameters.depth/2 + posArrXByZ[mesh][1] ); // the z coordinate, varMesh[mesh].height/2 makes the y coordinate act upon the closest side
scene.add( varMesh[mesh] ); // adds the specific mesh that was just positioned
}
};
HTML FILE
<!DOCTYPE html>
<html>
<head>
<title>Taro's World</title>
<style>
body {
margin: 0;
padding: 0;
border: 0;
}
</style>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.4/jquery.min.js"></script>
<script src="mrdoob-three.js-d6384d2/build/Three.js"></script>
<script src="mrdoob-three.js-d6384d2/examples/js/renderers/Projector.js"></script>
<script src="world.js"></script>
</head>
<body></body>
</html>
Two things are broken in your script :
in your positionThenAdd function, at position.set(...), you wrote somewhere ground.height. ground is an array, you probably meant varMesh[mesh].geometry.parameters.height.
your console should print that positionThenAdd is not a function. While you declared previous functions writing function myFunction(){....} you declared this one that way : var positionThenAdd = function () { ... };. The difference in javascript is that, as any variable, positionThenAdd will then be reachable in the script order. Since you write it at the end, nothing can reach it. You just have to modify its declaration to function positionThenAdd(){...}. See var functionName = function() {} vs function functionName() {}
Your scene : http://jsfiddle.net/ba8vvkyg/1/
In order to avoid the XY problem, let me explain where I'm coming from.
I would like to plot a large number of waveforms stacked on top of each other using the same time axis, using THREE.js. The waveforms are simply THREE.Line's and I am implementing zoom/pan/scaling of these waveforms by modifying the view bounds of an Orthographic camera.
My initial attempt at accomplishing this lead me to create multiple canvas elements with fixed height, stacked on top of each other, and attach a THREE.WebGLRenderer to each canvas.
This worked perfectly, until I tried scaling it past 15 or so waveforms, where THREE.js gave me a warning "too many active webgl contexts", and started deleting old contexts.
I feel like this is decent practice, considering it's the same technique applied here: http://threejs.org/examples/#webgl_multiple_canvases_grid
In this example, 4 WebGLRenderers are created, one for each canvas.
So, is it possible to override this warning somehow, and create an unbounded number of canvas elements, each with their own renderer?
ASIDE:
I have considered using one scene and positioning waveforms accordingly within it, and using multiple cameras with an approach similar to http://threejs.org/examples/#webgl_multiple_views.
The problems are two-fold:
(1) I lose the ability to dom-manipulate and easily attach key and mouse listeners on a per-waveform basis.
(2) This solution doesn't seem to scale either. Once the renderer's height passes somewhere around 6000px height, it starts to enter some type of corrupt state and part of the scene doesn't appear, with the rest of the content appearing stretched to compensate.
Thanks to anyone who can help!
You can use one non-scrolling full window size canvas, and place holders DIVs for your wave forms. Then with 1 renderer have 1 scene per waveform and call renderer.setViewport and renderer.setScissor with the location of each div before rendering each scene.
Effectively like this
renderer.setScissorTest( true );
scenes.forEach( function( scene ) {
// get the element that is a place holder for where we want to
// draw the scene
var viewElement = scene.viewElement;
// get its position relative to the page's viewport
var rect = viewElement.getBoundingClientRect();
// check if it's offscreen. If so skip it
if ( rect.bottom < 0 || rect.top > renderer.domElement.clientHeight ||
rect.right < 0 || rect.left > renderer.domElement.clientWidth ) {
return; // it's off screen
}
// set the viewport
var width = rect.right - rect.left;
var height = rect.bottom - rect.top;
var left = rect.left;
var top = rect.top;
renderer.setViewport( left, top, width, height );
renderer.setScissor( left, top, width, height );
camera.aspect = width / height;
camera.updateProjectionMatrix();
renderer.render( scene, camera );
} );
renderer.setScissorTest( false );
Example:
var canvas;
var scenes = [], camera, renderer, emptyScene;
init();
animate();
function init() {
canvas = document.getElementById( "c" );
camera = new THREE.PerspectiveCamera( 75, 1, 0.1, 100 );
camera.position.z = 1.5;
var geometries = [
new THREE.BoxGeometry( 1, 1, 1 ),
new THREE.SphereGeometry( 0.5, 12, 12 ),
new THREE.DodecahedronGeometry( 0.5 ),
new THREE.CylinderGeometry( 0.5, 0.5, 1, 12 ),
];
var template = document.getElementById("template").text;
var content = document.getElementById("content");
var emptyScene = new THREE.Scene();
var numScenes = 100;
for ( var ii = 0; ii < numScenes; ++ii ) {
var scene = new THREE.Scene();
// make a list item.
var element = document.createElement( "div" );
element.innerHTML = template;
element.className = "list-item";
// Look up the element that represents the area
// we want to render the scene
scene.element = element.querySelector(".scene");
content.appendChild(element);
// add one random mesh to each scene
var geometry = geometries[ geometries.length * Math.random() | 0 ];
var material = new THREE.MeshLambertMaterial( { color: randColor() } );
scene.add( new THREE.Mesh( geometry, material ) );
light = new THREE.DirectionalLight( 0xffffff );
light.position.set( 0.5, 0.8, 1 );
scene.add( light );
light = new THREE.DirectionalLight( 0xffffff );
light.position.set( -0.5, -0.8, -1 );
scene.add( light );
scenes.push( scene );
}
renderer = new THREE.WebGLRenderer( { canvas: canvas, antialias: true } );
renderer.setClearColor( 0xFFFFFF );
}
function updateSize() {
var width = canvas.clientWidth;
var height = canvas.clientHeight;
if ( canvas.width !== width || canvas.height != height ) {
renderer.setSize ( width, height, false );
}
}
function animate() {
render();
requestAnimationFrame( animate );
}
function render() {
updateSize();
canvas.style.transform = `translateY(${window.scrollY}px`;
renderer.setClearColor( 0xFFFFFF );
renderer.clear( true );
renderer.setClearColor( 0xE0E0E0 );
renderer.setScissorTest( true );
scenes.forEach( function( scene ) {
// so something moves
scene.children[0].rotation.x = Date.now() * 0.00111;
scene.children[0].rotation.z = Date.now() * 0.001;
// get the element that is a place holder for where we want to
// draw the scene
var element = scene.element;
// get its position relative to the page's viewport
var rect = element.getBoundingClientRect();
// check if it's offscreen. If so skip it
if ( rect.bottom < 0 || rect.top > renderer.domElement.clientHeight ||
rect.right < 0 || rect.left > renderer.domElement.clientWidth ) {
return; // it's off screen
}
// set the viewport
var width = rect.right - rect.left;
var height = rect.bottom - rect.top;
var left = rect.left;
var top = rect.top;
renderer.setViewport( left, top, width, height );
renderer.setScissor( left, top, width, height );
camera.aspect = width / height;
camera.updateProjectionMatrix();
renderer.render( scene, camera );
} );
renderer.setScissorTest( false );
}
function rand( min, max ) {
if ( max == undefined ) {
max = min;
min = 0;
}
return Math.random() * ( max - min ) + min;
}
function randColor() {
var colors = [ rand( 256 ), rand ( 256 ), rand( 256 ) ];
colors[ Math.random() * 3 | 0 ] = 255;
return ( colors[0] << 16 ) |
( colors[1] << 8 ) |
( colors[2] << 0 ) ;
}
* {
box-sizing: border-box;
-moz-box-sizing: border-box;
}
body {
color: #000;
font-family:Monospace;
font-size:13px;
background-color: #fff;
margin: 0;
}
#content {
position: absolute;
top: 0; width: 100%;
z-index: 1;
padding: 2em;
}
#c {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
}
.list-item {
margin: 1em;
padding: 2em;
display: -webkit-flex;
display: flex;
flex-direction: row;
-webkit-flex-direction: row;
}
.list-item .scene {
width: 200px;
height: 200px;
flex: 0 0 auto;
-webkit-flex: 0 0 auto;
}
.list-item .description {
font-family: sans-serif;
font-size: large;
padding-left: 2em;
flex: 1 1 auto;
-webkit-flex: 1 1 auto;
}
#media only screen and (max-width : 600px) {
#content {
width: 100%;
}
.list-item {
margin: 0.5em;
padding: 0.5em;
flex-direction: column;
-webkit-flex-direction: column;
}
.list-item .description {
padding-left: 0em;
}
}
<canvas id="c"></canvas>
<div id="content">
</div>
<script id="template" type="notjs">
<div class="scene"></div>
<div class="description">some random text about this object, scene, whatever</div>
</script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/94/three.min.js"></script>
Update:
The original solution here used a canvas with position: fixed meaning the canvas did not scroll. The new solution below changes it to position: absolute; top: 0 and then sets the canvas's transform every frame
canvas.style.transform = `translateY(${window.scrollY}px`;
This has the advantage that even if we can't update the canvas every frame the canvas will scroll with the page until we get a chance to update it. This makes the scrolling stay in sync.
You can compare the old solution to the new solution. Both are set to only render every 4th frame to exaggerate the issue. Scroll them up and down and the difference should be clear.
Update 2:
Yet another solution is to virtualize the WebGL context in which you create one offscreen WebGL context and then patch things so other other uses of WebGL get a virtual WebGL context that is simulated on top of a single shared actual WebGL context.