Update 5
Created another fiddle to show what is expected would look like. An invisible skydome and a cubecamera are added and environment map is used; in my case, none of these technique should be used for the reasons already mentioned.
var MatcapTransformer = function(uvs, face) {
for (var i = uvs.length; i-- > 0;) {
uvs[i].x = face.vertexNormals[i].x * 0.5 + 0.5;
uvs[i].y = face.vertexNormals[i].y * 0.5 + 0.5;
}
};
var TransformUv = function(geometry, xformer) {
// The first argument is also used as an array in the recursive calls
// as there's no method overloading in javascript; and so is the callback.
var a = arguments[0],
callback = arguments[1];
var faceIterator = function(uvFaces, index) {
xformer(uvFaces[index], geometry.faces[index]);
};
var layerIterator = function(uvLayers, index) {
TransformUv(uvLayers[index], faceIterator);
};
for (var i = a.length; i-- > 0;) {
callback(a, i);
}
if (!(i < 0)) {
TransformUv(geometry.faceVertexUvs, layerIterator);
}
};
var SetResizeHandler = function(renderer, camera) {
var callback = function() {
renderer.setSize(window.innerWidth, window.innerHeight);
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
};
// bind the resize event
window.addEventListener('resize', callback, false);
// return .stop() the function to stop watching window resize
return {
stop: function() {
window.removeEventListener('resize', callback);
}
};
};
(function() {
var fov = 45;
var aspect = window.innerWidth / window.innerHeight;
var loader = new THREE.TextureLoader();
var texture = loader.load('https://i.postimg.cc/mTsN30vx/canyon-s.jpg');
texture.wrapS = THREE.RepeatWrapping;
texture.wrapT = THREE.RepeatWrapping;
texture.center.set(1 / 2, 1 / 2);
var cubeCam = new THREE.CubeCamera(.1, 200, 4096);
cubeCam.renderTarget.texture.wrapS = THREE.RepeatWrapping;
cubeCam.renderTarget.texture.wrapT = THREE.RepeatWrapping;
cubeCam.renderTarget.texture.center.set(1 / 2, 1 / 2);
var geoSky = new THREE.SphereGeometry(2, 16, 16);
var matSky = new THREE.MeshBasicMaterial({
'map': texture,
'side': THREE.BackSide
});
var meshSky = new THREE.Mesh(geoSky, matSky);
meshSky.visible = false;
var geometry = new THREE.IcosahedronGeometry(1, 1);
var material = new THREE.MeshBasicMaterial({
'envMap': cubeCam.renderTarget.texture
});
var mesh = new THREE.Mesh(geometry, material);
var geoWireframe = new THREE.WireframeGeometry(geometry);
var matWireframe = new THREE.LineBasicMaterial({
'color': 'red',
'linewidth': 2
});
mesh.add(new THREE.LineSegments(geoWireframe, matWireframe));
var camera = new THREE.PerspectiveCamera(fov, aspect);
camera.position.setZ(20);
var scene = new THREE.Scene();
scene.add(mesh);
scene.add(meshSky);
{
var mirror = new THREE.CubeCamera(.1, 2000, 4096);
var geoPlane = new THREE.PlaneGeometry(16, 16);
var matPlane = new THREE.MeshBasicMaterial({
'envMap': mirror.renderTarget.texture
});
var plane = new THREE.Mesh(geoPlane, matPlane);
plane.add(mirror);
plane.position.setZ(-4);
plane.lookAt(mesh.position);
scene.add(plane);
}
var renderer = new THREE.WebGLRenderer();
var container = document.getElementById('container1');
container.appendChild(renderer.domElement);
SetResizeHandler(renderer, camera);
renderer.setSize(window.innerWidth, window.innerHeight);
var controls = new THREE.TrackballControls(camera, container);
var fixTextureWhenRotateAroundAllAxis = function() {
mesh.rotation.y += 0.01;
mesh.rotation.x += 0.01;
mesh.rotation.z += 0.01;
cubeCam.update(renderer, scene);
};
renderer.setAnimationLoop(function() {
// controls.update();
plane.visible = false;
{
meshSky.visible = true;
mesh.visible = false;
fixTextureWhenRotateAroundAllAxis();
mesh.visible = true;
meshSky.visible = false;
}
mirror.update(renderer, scene);
plane.visible = true;
renderer.render(scene, camera);
});
})();
body {
background-color: #000;
margin: 0px;
overflow: hidden;
}
<script src="https://threejs.org/build/three.min.js"></script>
<script src="https://threejs.org/examples/js/controls/TrackballControls.js"></script>
<div id='container1'></div>
Update 4
Important: Please note there is a reflective plane in back of the target mesh which is for observing if the texture binds to the mesh surface correctly, it has nothing to do with what I'm trying to solve.
Update 3
Created a new fiddle to show what is NOT the expected behaviour
Code
var MatcapTransformer=function(uvs, face) {
for(var i=uvs.length; i-->0;) {
uvs[i].x=face.vertexNormals[i].x*0.5+0.5;
uvs[i].y=face.vertexNormals[i].y*0.5+0.5;
}
};
var TransformUv=function(geometry, xformer) {
// The first argument is also used as an array in the recursive calls
// as there's no method overloading in javascript; and so is the callback.
var a=arguments[0], callback=arguments[1];
var faceIterator=function(uvFaces, index) {
xformer(uvFaces[index], geometry.faces[index]);
};
var layerIterator=function(uvLayers, index) {
TransformUv(uvLayers[index], faceIterator);
};
for(var i=a.length; i-->0;) {
callback(a, i);
}
if(!(i<0)) {
TransformUv(geometry.faceVertexUvs, layerIterator);
}
};
var SetResizeHandler=function(renderer, camera) {
var callback=function() {
renderer.setSize(window.innerWidth, window.innerHeight);
camera.aspect=window.innerWidth/window.innerHeight;
camera.updateProjectionMatrix();
};
// bind the resize event
window.addEventListener('resize', callback, false);
// return .stop() the function to stop watching window resize
return {
stop: function() {
window.removeEventListener('resize', callback);
}
};
};
var getVertexShader=function() {
return `
void main() {
gl_Position=projectionMatrix*modelViewMatrix*vec4(position, 1.0);
}
`;
};
var getFragmentShader=function(size) {
return `
uniform sampler2D texture1;
const vec2 size=vec2(`+size.x+`, `+size.y+`);
void main() {
gl_FragColor=texture2D(texture1, gl_FragCoord.xy/size.xy);
}
`;
};
(function() {
var fov=45;
var aspect=window.innerWidth/window.innerHeight;
var loader=new THREE.TextureLoader();
var texture=loader.load('https://i.postimg.cc/mTsN30vx/canyon-s.jpg');
texture.wrapS=THREE.RepeatWrapping;
texture.wrapT=THREE.RepeatWrapping;
texture.center.set(1/2, 1/2);
var geometry=new THREE.SphereGeometry(1, 16, 16);
// var geometry=new THREE.BoxGeometry(2, 2, 2);
// var material=new THREE.MeshBasicMaterial({ 'map': texture });
var material=new THREE.ShaderMaterial({
'uniforms': { 'texture1': { 'type': 't', 'value': texture } }
, 'vertexShader': getVertexShader()
, 'fragmentShader': getFragmentShader({ 'x': 512, 'y': 256 })
});
var mesh=new THREE.Mesh(geometry, material);
var geoWireframe=new THREE.WireframeGeometry(geometry);
var matWireframe=new THREE.LineBasicMaterial({ 'color': 'red', 'linewidth': 2 });
mesh.add(new THREE.LineSegments(geoWireframe, matWireframe));
var camera=new THREE.PerspectiveCamera(fov, aspect);
camera.position.setZ(20);
var scene=new THREE.Scene();
scene.add(mesh);
{
var mirror=new THREE.CubeCamera(.1, 2000, 4096);
var geoPlane=new THREE.PlaneGeometry(16, 16);
var matPlane=new THREE.MeshBasicMaterial({
'envMap': mirror.renderTarget.texture
});
var plane=new THREE.Mesh(geoPlane, matPlane);
plane.add(mirror);
plane.position.setZ(-4);
plane.lookAt(mesh.position);
scene.add(plane);
}
var renderer=new THREE.WebGLRenderer();
var container=document.getElementById('container1');
container.appendChild(renderer.domElement);
SetResizeHandler(renderer, camera);
renderer.setSize(window.innerWidth, window.innerHeight);
var fixTextureWhenRotateAroundYAxis=function() {
mesh.rotation.y+=0.01;
texture.offset.set(mesh.rotation.y/(2*Math.PI), 0);
};
var fixTextureWhenRotateAroundZAxis=function() {
mesh.rotation.z+=0.01;
texture.rotation=-mesh.rotation.z
TransformUv(geometry, MatcapTransformer);
};
var fixTextureWhenRotateAroundAllAxis=function() {
mesh.rotation.y+=0.01;
mesh.rotation.x+=0.01;
mesh.rotation.z+=0.01;
};
var controls=new THREE.TrackballControls(camera, container);
renderer.setAnimationLoop(function() {
fixTextureWhenRotateAroundAllAxis();
controls.update();
plane.visible=false;
mirror.update(renderer, scene);
plane.visible=true;
renderer.render(scene, camera);
});
})();
body {
background-color: #000;
margin: 0px;
overflow: hidden;
}
<script src="https://threejs.org/build/three.min.js"></script>
<script src="https://threejs.org/examples/js/controls/TrackballControls.js"></script>
<div id='container1'></div>
Maybe I should rephrase my question, but I lack the knowledge to describe accurately about what I'm trying to solve, please help .. (Panoramic-Transform-With-Texture-Looking-At-Direction-Locked-Onto-The-Camera maybe .. ?)
Update 2
(Has deprecated as code snippet is applied. )
Update
OK .. I've added 3 methods:
TransformUv accepts a geometry, and a transformer method which handles uv-transform. The callback accepts an uvs array for each face and the corresponding Face3 of geometry.faces[] as its parameters.
MatcapTransformer is the uv-transform handler callback to do the matcap transform.
and
fixTextureWhenRotateAroundZAxis works like what it named.
So far none of the fixTexture.. methods can work alltogether, also, fixTextureWhenRotateAroundXAxis is not figured out. The problem remains unsolved, I wish what's just added could help you to help me out.
I'm trying to make the texture of a mesh always face an active perspective camera, no matter what are the relative positions.
For constructing a real case of my scene and the interaction would be quite complex, I built a minimal example to demonstrate my intention.
Code
var MatcapTransformer=function(uvs, face) {
for(var i=uvs.length; i-->0;) {
uvs[i].x=face.vertexNormals[i].x*0.5+0.5;
uvs[i].y=face.vertexNormals[i].y*0.5+0.5;
}
};
var TransformUv=function(geometry, xformer) {
// The first argument is also used as an array in the recursive calls
// as there's no method overloading in javascript; and so is the callback.
var a=arguments[0], callback=arguments[1];
var faceIterator=function(uvFaces, index) {
xformer(uvFaces[index], geometry.faces[index]);
};
var layerIterator=function(uvLayers, index) {
TransformUv(uvLayers[index], faceIterator);
};
for(var i=a.length; i-->0;) {
callback(a, i);
}
if(!(i<0)) {
TransformUv(geometry.faceVertexUvs, layerIterator);
}
};
var SetResizeHandler=function(renderer, camera) {
var callback=function() {
renderer.setSize(window.innerWidth, window.innerHeight);
camera.aspect=window.innerWidth/window.innerHeight;
camera.updateProjectionMatrix();
};
// bind the resize event
window.addEventListener('resize', callback, false);
// return .stop() the function to stop watching window resize
return {
stop: function() {
window.removeEventListener('resize', callback);
}
};
};
(function() {
var fov=45;
var aspect=window.innerWidth/window.innerHeight;
var loader=new THREE.TextureLoader();
var texture=loader.load('https://i.postimg.cc/mTsN30vx/canyon-s.jpg');
texture.wrapS=THREE.RepeatWrapping;
texture.wrapT=THREE.RepeatWrapping;
texture.center.set(1/2, 1/2);
var geometry=new THREE.SphereGeometry(1, 16, 16);
var material=new THREE.MeshBasicMaterial({ 'map': texture });
var mesh=new THREE.Mesh(geometry, material);
var geoWireframe=new THREE.WireframeGeometry(geometry);
var matWireframe=new THREE.LineBasicMaterial({ 'color': 'red', 'linewidth': 2 });
mesh.add(new THREE.LineSegments(geoWireframe, matWireframe));
var camera=new THREE.PerspectiveCamera(fov, aspect);
camera.position.setZ(20);
var scene=new THREE.Scene();
scene.add(mesh);
{
var mirror=new THREE.CubeCamera(.1, 2000, 4096);
var geoPlane=new THREE.PlaneGeometry(16, 16);
var matPlane=new THREE.MeshBasicMaterial({
'envMap': mirror.renderTarget.texture
});
var plane=new THREE.Mesh(geoPlane, matPlane);
plane.add(mirror);
plane.position.setZ(-4);
plane.lookAt(mesh.position);
scene.add(plane);
}
var renderer=new THREE.WebGLRenderer();
var container=document.getElementById('container1');
container.appendChild(renderer.domElement);
SetResizeHandler(renderer, camera);
renderer.setSize(window.innerWidth, window.innerHeight);
var fixTextureWhenRotateAroundYAxis=function() {
mesh.rotation.y+=0.01;
texture.offset.set(mesh.rotation.y/(2*Math.PI), 0);
};
var fixTextureWhenRotateAroundZAxis=function() {
mesh.rotation.z+=0.01;
texture.rotation=-mesh.rotation.z
TransformUv(geometry, MatcapTransformer);
};
// This is wrong
var fixTextureWhenRotateAroundAllAxis=function() {
mesh.rotation.y+=0.01;
mesh.rotation.x+=0.01;
mesh.rotation.z+=0.01;
// Dun know how to do it correctly ..
texture.offset.set(mesh.rotation.y/(2*Math.PI), 0);
};
var controls=new THREE.TrackballControls(camera, container);
renderer.setAnimationLoop(function() {
fixTextureWhenRotateAroundYAxis();
// Uncomment the following line and comment out `fixTextureWhenRotateAroundYAxis` to see the demo
// fixTextureWhenRotateAroundZAxis();
// fixTextureWhenRotateAroundAllAxis();
// controls.update();
plane.visible=false;
mirror.update(renderer, scene);
plane.visible=true;
renderer.render(scene, camera);
});
})();
body {
background-color: #000;
margin: 0px;
overflow: hidden;
}
<script src="https://threejs.org/build/three.min.js"></script>
<script src="https://threejs.org/examples/js/controls/TrackballControls.js"></script>
<div id='container1'></div>
Please note that although the mesh itself rotates in this demonstration, my real intention is making the camera move like orbiting around the mesh.
I've added the wireframe to make the movement more clear. As you can see
I use fixTextureWhenRotateAroundYAxis to do it correctly, but it's only for the y-axis. The mesh.rotation.y in my real code is calculated something like
var ve=camera.position.clone();
ve.sub(mesh.position);
var rotY=Math.atan2(ve.x, ve.z);
var offsetX=rotY/(2*Math.PI);
However, I lack the knowledge of how to do fixTextureWhenRotateAroundAllAxis correctly. There are some restrictions of solving this:
CubeCamera/CubeMap cannot be used as the client machines might have performance issues
Do not simply make the mesh lookAt the camera as they are eventually of any kind of geometry, not only the spheres; tricks like lookAt and restore .quaternion in a frame would be ok.
Please don't get me wrong that I'm asking an XY problem as I don't have the right to expose proprietary code or I wouldn't have to pay the effort to build a minimal example :)
Facing the camera will look like:
Or, even better, as in this question, where the opposite fix is asked:
To achieve that, you have to setup a simple fragment shader (as the OP
accidentally did):
Vertex shader
void main() {
gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
}
Fragment shader
uniform vec2 size;
uniform sampler2D texture;
void main() {
gl_FragColor = texture2D(texture, gl_FragCoord.xy / size.xy);
}
A working mock of the shader with Three.js
function main() {
// Uniform texture setting
const uniforms = {
texture1: { type: "t", value: new THREE.TextureLoader().load( "https://threejsfundamentals.org/threejs/resources/images/wall.jpg" ) }
};
// Material by shader
const myMaterial = new THREE.ShaderMaterial({
uniforms: uniforms,
vertexShader: document.getElementById('vertexShader').textContent,
fragmentShader: document.getElementById('fragmentShader').textContent
});
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 = 2;
const scene = new THREE.Scene();
const boxWidth = 1;
const boxHeight = 1;
const boxDepth = 1;
const geometry = new THREE.BoxGeometry(boxWidth, boxHeight, boxDepth);
const cubes = []; // just an array we can use to rotate the cubes
const cube = new THREE.Mesh(geometry, myMaterial);
scene.add(cube);
cubes.push(cube); // add to our list of cubes to rotate
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();
}
cubes.forEach((cube, ndx) => {
const speed = .2 + 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;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/109/three.min.js"></script>
<script id="vertexShader" type="x-shader/x-vertex">
void main() {
gl_Position = projectionMatrix *
modelViewMatrix *
vec4(position,1.0);
}
</script>
<script id="fragmentShader" type="x-shader/x-fragment">
uniform sampler2D texture1;
const vec2 size = vec2(1024, 512);
void main() {
gl_FragColor = texture2D(texture1,gl_FragCoord.xy/size.xy);
}
</script>
<canvas id="c"></canvas>
A viable alternative: Cube Mapping
Here I've modified a jsfiddle about cube mapping, maybe is what are you looking for:
https://jsfiddle.net/v8efxdo7/
The cube project its face texture on the underlying object and it's looking at the camera.
Note: lights changes with rotation because light and inner object are in fixed position, while camera and projection cube rotates both around the center of the scene.
If you carefully look to the example, this technique is not perfect, but what are you looking for (applied to a box) is tricky, because the UV unwrap of the texture of a cube is cross-shaped, rotating the UV itself will not be effective and using projection techniques has its drawbacks too, because the projector object shape and projection subject shape matters.
Just for better understanding: in the real world, where do you see this effect in 3d space on boxes ? The only example that comes in my mind is a 2D projection on a 3D surface (like projection mapping in visual design).
Related
I developed a 3D cube that moves on one axis simulating the accelerometer sensor.
Currently, I have the .obj and .mtl of a toycar which I aim to add to the scene(), but when I remove the
BoxGeometry 3D cube and replace it with the car mesh I got these errors all the time:
I also get this error saying that obj is not defined, even if I defined it globally I still have the same issue:
I checked libraries that exist locally and other function but I can't see where is the problem.
Below is how I load the car model:
const scene = new THREE.Scene();
var loader = new THREE.OBJMTLLoader();
loader.load('https://jyunming-chen.github.io/tutsplus/models/toycar.obj', 'https://jyunming-chen.github.io/tutsplus/models/toycar.mtl',
function (vehicle) {
toycar = vehicle;
toycar.rotateY(-10.99);
scene.add(toycar);
});
and this is my full .HTML code with js implementation:
This is hoq looks like now:
and this is what I am aiming to achieve:
That's my current code:
<html>
<head>
<meta charset="UTF-8">
<script src="./three.min.js"></script>
<script src="./require.js" type="text/javascript"></script>
<script src="./OrbitControls.js"></script>
<script src="./KeyboardState.js"></script>
<script src="./MTLLoader.js"></script>
<script src="./OBJMTLLoader.js"></script>
<script type="module"> import * as THREE from "./three.module.js"</script>
</head>
<body>
<canvas id="canvas" width="1000" height="600" style="border:1px solid #000000;"></canvas>
</body>
<script>
let sensorValue = 0;
let sensorAddr = 0;
var toycar;
StartRetrieveLiveData();
function main() {
const canvas = document.querySelector('#canvas');
const accelPanel = document.querySelector('#accelPanel');
const renderer = new THREE.WebGLRenderer({ alpha: true, canvas });
renderer.setClearColor( 0x626d73, 1 );
var context = canvas.getContext("2d");
var width = window.innerWidth;
var height = window.innerHeight;
const fov = 70;
const aspect = 2;
const near = 20;
const far = 500;
const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
camera.position.set(0, 50, 1.5);
camera.up.set(0, 0, 1);
camera.lookAt(0, 0, 0);
const scene = new THREE.Scene();
// var loader = new THREE.OBJMTLLoader();
// loader.load('https://jyunming-chen.github.io/tutsplus/models/toycar.obj', 'https://jyunming-chen.github.io/tutsplus/models/toycar.mtl',
// function (vehicle) {
// toycar = vehicle;
// toycar.rotateY(-10.99);
// scene.add(toycar);
// });
// An array of objects who's rotation to update
const objects = [];
const radius = 3;
const widthSegments = 3;
const heightSegments = 3;
const sphereGeometry = new THREE.BoxGeometry(radius, widthSegments, heightSegments);
const sunMaterial = new THREE.MeshBasicMaterial({ color: "green", wireframe: false });
const object = new THREE.Mesh(sphereGeometry, sunMaterial);
var cubeAxis = new THREE.AxesHelper(10);
object.add(cubeAxis);
object.scale.set(2, 2, 2);
scene.add(object);
objects.push(object);
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();
}
objects.forEach((obj) => {
sensorValueIndex = ((sensorValue / 16384) * 10);
obj.position.z = ((sensorValue / 16384) * 20);
console.log("AccX: ",sensorValueIndex);
// // Here I take accelerometerX and pass them to the 3D model
// if (sensorAddr === 1) {
// }
});
renderer.render(scene, camera);
requestAnimationFrame(render);
}
requestAnimationFrame(render);
}
function onMsg(event) {
// console.log(`[message] Data received from server: ${event.data}`);
// console.log("event.data = " + JSON.parse(event.data));
var received_msg = event.data;
var obj = JSON.parse(JSON.parse(received_msg));
if (obj !== null) {
if (
obj.hasOwnProperty("DataMapChangedObjectsAddressValue") &&
obj["DataMapChangedObjectsAddressValue"][0]["DataMapAddress"] !==
undefined
) {
sensorAddr =
obj["DataMapChangedObjectsAddressValue"][0]["DataMapAddress"];
sensorValue =
obj["DataMapChangedObjectsAddressValue"][0]["Value"];
// if (sensorAddr === 1) {
// sensorValueIndex = (sensorValue / 16384) * 500;
// }
}
}
}
function onOpen(e) {
console.log("SSE connected");
}
function onError(e) {
// console.log(`[error] ${error.message}`);
if (e.eventPhase == EventSource.CLOSED) this.source.close();
if (e.target.readyState == EventSource.CLOSED) {
console.log("SSE Disconnected");
} else if (e.target.readyState == EventSource.CONNECTING) {
console.log("SSE Connecting ...");
}
}
function StartRetrieveLiveData() {
if (!!window.EventSource) {
this.source = new EventSource("/sse");
} else {
console.log("Your browser doesn't support SSE");
}
this.source.addEventListener("message", e => this.onMsg(e));
this.source.addEventListener("open", e => this.onOpen(e), false);
this.source.addEventListener("error", e => this.onError(e), false);
// Add here (only mozilla)
main();
// Add here
}
</script>
</html>
Note that when I used a public server the whole thing works just fine, but I used the remote server (the actual server) I get these error and the whole thing doesn't work as expected.
Would appreciate a solution for this.
I solved the problem so I am going to answer this.
Firstly, the document.querySelector('#canvas') should be removed to avoid creating two canvas, because I am calling the libraries in the . This was a silly mistake but I found it finally.
Secondly, the camera position was off and it wasn't really pointing at the car so nothing was shown on the screen. I tweaked the camera.position on the X, Y and Z axis to get it right.
There were bunches of other silly mistakes I found while carefully debugging the code, I am not an expert in the language yet so these mistakes serve as a learning experience for me.
The final working code is below:
let sensorValue = 0;
var toycar;
StartRetrieveLiveData();
var scene, renderer, camera;
var controls, keyboard = new KeyboardState();
var toycar;
function init() {
var width = window.innerWidth;
var height = window.innerHeight;
renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
renderer.setClearColor(0x626d73, 1);
renderer.setSize(width, height);
document.body.appendChild(renderer.domElement);
scene = new THREE.Scene();
camera = new THREE.PerspectiveCamera(10, width / height, 1, 10000);
camera.position.y = -150;
camera.lookAt(new THREE.Vector3(0, 0, 0));
var loader = new THREE.OBJMTLLoader();
loader.load('./toycar.obj', './toycar.mtl',
function (object) {
toycar = object;
toycar.rotateZ(10.99); //toycar.rotateZ(-10.99);
scene.add(toycar);
});
var gridXZ = new THREE.GridHelper(350000, 10000);
gridXZ.setColors(new THREE.Color(0xff0000), new THREE.Color(0xffffff));
scene.add(gridXZ);
var pointLight = new THREE.PointLight(0xffffff);
pointLight.position.set(350, 20, 5);
scene.add(pointLight);
var ambientLight = new THREE.AmbientLight(0x111111);
scene.add(ambientLight);
}
function animate() {
var angle = 0;
var speed = 0;
var pos = new THREE.Vector3(0, 0, 0);
var clock = new THREE.Clock();
var dt = clock.getDelta();
var dir = new THREE.Vector3(1, 0, 0);
dir.multiplyScalar(dt * speed);
dir.applyAxisAngle(new THREE.Vector3(0, 0, 0), 10);
pos.add(dir);
if (toycar != undefined) {
sensorValueIndex = ((sensorValue / 16384) * 50);
toycar.scale.set(0.1, 0.1, 0.1);
toycar.position.x = sensorValueIndex;
toycar.position.y = 0;
toycar.position.z = 0;
toycar.rotation.x = (angle + Math.PI);
}
requestAnimationFrame(animate);
renderer.render(scene, camera);
}
function onMsg(event) {
var received_msg = event.data;
var obj = JSON.parse(JSON.parse(received_msg));
if (obj !== null) {
if (
obj.hasOwnProperty("DataMapChangedObjectsAddressValue") &&
obj["DataMapChangedObjectsAddressValue"][0]["DataMapAddress"] !==
undefined
) {
let sensorAddr =
obj["DataMapChangedObjectsAddressValue"][0]["DataMapAddress"];
sensorValue =
obj["DataMapChangedObjectsAddressValue"][0]["Value"];
if (sensorAddr === 1) {
sensorValueIndex = (sensorValue / 16384) * 10;
console.log(sensorValueIndex);
}
}
}
}
I'm trying to raycast skinning mesh (of knowing issue) after some skeleton changes (without animation on it, so performance isn't a priority).
The tricky thing i imagine in this attempt is:
Load skinned mesh add to scene
Make some changes in positions of specific bones at loaded mesh
Copy geometries of transformed loaded mesh (maybe from buffer?)
Create new mesh (some kind of imitation ghost mesh) from copied geometries and apply to it
set raycast on ghost mesh with opacity material= 0.0
Above list should work, but I'm stuck third day on point 3 cause I can't get transformed vertices after skinning.
var scene, camera, renderer, mesh, ghostMesh;
var raycaster = new THREE.Raycaster();
var raycasterMeshHelper;
initScene();
render();
function initScene() {
scene = new THREE.Scene();
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 200);
camera.position.set(20, 7, 3);
renderer = new THREE.WebGLRenderer({
antialias: true
});
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
window.addEventListener('resize', onWindowResize, false);
var orbit = new THREE.OrbitControls(camera, renderer.domElement);
//lights stuff
var ambientLight = new THREE.AmbientLight(0xffffff, 0.3);
scene.add(ambientLight);
var lights = [];
lights[0] = new THREE.PointLight(0xffffff, 1, 0);
lights[1] = new THREE.PointLight(0xffffff, 1, 0);
lights[2] = new THREE.PointLight(0xffffff, 1, 0);
lights[0].position.set(0, 200, 0);
lights[1].position.set(100, 200, 100);
lights[2].position.set(-100, -200, -100);
scene.add(lights[0]);
scene.add(lights[1]);
scene.add(lights[2]);
//raycaster mesh
var raycasterMaterial = new THREE.MeshBasicMaterial({
color: 0xdddddd,
opacity: 0.7,
transparent: true
});
var geometrySphere = new THREE.SphereGeometry(0.5, 16, 16);
raycasterMeshHelper = new THREE.Mesh(geometrySphere, raycasterMaterial);
raycasterMeshHelper.visible = false;
scene.add(raycasterMeshHelper);
renderer.domElement.addEventListener('mousemove', onMouseMove, false);
//model Loading
var loader = new THREE.JSONLoader();
loader.load("https://raw.githubusercontent.com/visus100/skinnedTests/master/js_fiddle/skinned_mesh.json", function(geometry) {
var meshMaterial = new THREE.MeshStandardMaterial({
color: 0x00df15,
skinning: true
});
mesh = new THREE.SkinnedMesh(geometry, meshMaterial);
scene.add(mesh);
var skeleton = new THREE.SkeletonHelper(mesh);
scene.add(skeleton);
//some experimental skeletonal changes
mesh.skeleton.bones[1].rotation.z += 0.10;
mesh.skeleton.bones[2].rotation.x += -0.65;
mesh.skeleton.bones[3].rotation.y += -0.45;
mesh.skeleton.bones[3].position.x += 0.11;
//updates matrix
mesh.updateMatrix();
mesh.geometry.applyMatrix(mesh.matrix);
mesh.updateMatrixWorld(true);
//crate ghost mesh geometry
createGhostMesh();
//crate point cloud helper from buffergeometry
var bufferGeometry = new THREE.BufferGeometry().fromGeometry(mesh.geometry);
var particesMaterial = new THREE.PointsMaterial({
color: 0xff00ea,
size: 0.07,
sizeAttenuation: false
});
particles = new THREE.Points(bufferGeometry, particesMaterial);
particles.sortParticles = true;
scene.add(particles);
});
}
function createGhostMesh() {
var geometryForGhostMesh = new THREE.Geometry();
//push vertices and other stuff to geometry
for (i = 0; i < mesh.geometry.vertices.length; i++) {
var temp = new THREE.Vector3(mesh.geometry.vertices[i].x, mesh.geometry.vertices[i].y, mesh.geometry.vertices[i].z);
geometryForGhostMesh.vertices.push(temp);
//////
//here should be the code for calc translation vertices of skinned mesh and added to geometryForGhostMesh
//////
geometryForGhostMesh.skinIndices.push(mesh.geometry.skinIndices[i]);
geometryForGhostMesh.skinWeights.push(mesh.geometry.skinWeights[i]);
}
for (i = 0; i < mesh.geometry.faces.length; i++) {
geometryForGhostMesh.faces.push(mesh.geometry.faces[i]);
}
//create material and add to scene
var ghostMaterial = new THREE.MeshBasicMaterial({
color: 0xff0000,
opacity: 0.1,
transparent: true,
skinning: true
});
ghostMesh = new THREE.Mesh(geometryForGhostMesh, ghostMaterial);
scene.add(ghostMesh);
}
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}
function render() {
requestAnimationFrame(render);
renderer.render(scene, camera);
};
function onMouseMove(event) {
//raycaster for ghostMesh
if (ghostMesh) {
var rect = renderer.domElement.getBoundingClientRect();
var mouseX = ((event.clientX - rect.left) / rect.width) * 2 - 1;
var mouseY = -((event.clientY - rect.top) / rect.height) * 2 + 1;
raycaster.setFromCamera(new THREE.Vector2(mouseX, mouseY), camera);
var intersects = raycaster.intersectObject(ghostMesh);
if (intersects.length > 0) {
raycasterMeshHelper.visible = true;
raycasterMeshHelper.position.set(0, 0, 0);
raycasterMeshHelper.lookAt(intersects[0].face.normal);
raycasterMeshHelper.position.copy(intersects[0].point);
} else {
raycasterMeshHelper.visible = false;
}
}
}
body {
margin: 0px;
background-color: #000000;
overflow: hidden;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/98/three.min.js"></script>
<script src="https://threejs.org/examples/js/controls/OrbitControls.js"></script>
Please note that I need this in thre.js build r98 or less, because the rest of my code (not included here) and without morph tangents only skinning bones.
I tried to write it clearly and please if anyone want help do it so because I'm not a pro.
I not including my approach of calculating transformed geometries because I failed too hard.
I dug a lot about this problem here e.g. issue6440 and for today it's still not fixed.
But there existing methods to work with it e.g https://jsfiddle.net/fnjkeg9x/1/ but after several of attempts I failed and my conclusion is that the stormtrooper works on morph tanges and this could be the reason I failed.
EDIT:
I created next codepen based on this topics get-the-global-position-of-a-vertex-of-a-skinned-mesh and Stormtrooper.
Decided to start with simple box to make bounding around skinned transformed mesh.
Result is fail because it giving 0 at line:
boneMatrix.fromArray(skeleton.boneMatrices, si * 16);
Here i comparing stormtrooper with my example output from console: Screen shot image
Codpen with new progress: https://codepen.io/donkeyLuck0/pen/XQbBMQ
My other idea is to apply this bones form loaded model and rig as a morph tangent programmatically (but i don't even know if it is possible and how to figure it out)
Founded example of animated model
Sketchfab animation with points tracking
This is super late to the game, but here's an example of GPU picking that works with skinned meshes and doesn't require a separate picking scene to keep in sync with your main scene, nor does it require the user to manage custom materials:
https://github.com/bzztbomb/three_js_gpu_picking
The trick that allows for easy material overriding and scene re-use is here:
https://github.com/bzztbomb/three_js_gpu_picking/blob/master/gpupicker.js#L58
A proper support for raycasting for skinned meshes was added in https://github.com/mrdoob/three.js/pull/19178 in revision 116.
You can use GPU picking to "pick" skinned object. It won't give you a position though
Note: GPU picking requires rendering every pickable object with a custom material. How you implement that is up to you. This article does it by making 2 scenes. That might not be as useful for skinned objects.
Unfortunately three.js provides no way to override materials AFAICT. Here's an example that replaces the materials on the pickable objects before rendering for picking and then restores them after. You would also need to hide any objects you don't want picked.
const renderer = new THREE.WebGLRenderer({
antialias: true,
canvas: document.querySelector('canvas'),
});
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, 2, 0.1, 200);
camera.position.set(20, 7, 3);
const orbit = new THREE.OrbitControls(camera, renderer.domElement);
//lights stuff
const ambientLight = new THREE.AmbientLight(0xffffff, 0.3);
scene.add(ambientLight);
const lights = [];
lights[0] = new THREE.PointLight(0xffffff, 1, 0);
lights[1] = new THREE.PointLight(0xffffff, 1, 0);
lights[2] = new THREE.PointLight(0xffffff, 1, 0);
lights[0].position.set(0, 200, 0);
lights[1].position.set(100, 200, 100);
lights[2].position.set(-100, -200, -100);
scene.add(lights[0]);
scene.add(lights[1]);
scene.add(lights[2]);
//raycaster mesh
const raycasterMaterial = new THREE.MeshBasicMaterial({
color: 0xdddddd,
opacity: 0.7,
transparent: true
});
const geometrySphere = new THREE.SphereGeometry(0.5, 16, 16);
raycasterMeshHelper = new THREE.Mesh(geometrySphere, raycasterMaterial);
raycasterMeshHelper.visible = false;
scene.add(raycasterMeshHelper);
//model Loading
const pickableObjects = [];
const loader = new THREE.JSONLoader();
loader.load("https://raw.githubusercontent.com/visus100/skinnedTests/master/js_fiddle/skinned_mesh.json", (geometry) => {
const meshMaterial = new THREE.MeshStandardMaterial({
color: 0x00df15,
skinning: true
});
const mesh = new THREE.SkinnedMesh(geometry, meshMaterial);
scene.add(mesh);
const id = pickableObjects.length + 1;
pickableObjects.push({
mesh,
renderingMaterial: meshMaterial,
pickingMaterial: new THREE.MeshPhongMaterial({
skinning: true,
emissive: new THREE.Color(id),
color: new THREE.Color(0, 0, 0),
specular: new THREE.Color(0, 0, 0),
//map: texture,
//transparent: true,
//side: THREE.DoubleSide,
//alphaTest: 0.5,
blending: THREE.NoBlending,
}),
});
//some experimental skeletonal changes
mesh.skeleton.bones[1].rotation.z += 0.10;
mesh.skeleton.bones[2].rotation.x += -0.65;
mesh.skeleton.bones[3].rotation.y += -0.45;
mesh.skeleton.bones[3].position.x += 0.11;
//updates matrix
mesh.updateMatrix();
mesh.geometry.applyMatrix(mesh.matrix);
mesh.updateMatrixWorld(true);
});
class GPUPickHelper {
constructor() {
// create a 1x1 pixel render target
this.pickingTexture = new THREE.WebGLRenderTarget(1, 1);
this.pixelBuffer = new Uint8Array(4);
}
pick(cssPosition, scene, camera) {
const {
pickingTexture,
pixelBuffer
} = this;
// set the view offset to represent just a single pixel under the mouse
const pixelRatio = renderer.getPixelRatio();
camera.setViewOffset(
renderer.context.drawingBufferWidth, // full width
renderer.context.drawingBufferHeight, // full top
cssPosition.x * pixelRatio | 0, // rect x
cssPosition.y * pixelRatio | 0, // rect y
1, // rect width
1, // rect height
);
// render the scene
// r102
//renderer.setRenderTarget(pickingTexture);
//renderer.render(scene, camera);
//renderer.setRenderTarget(null);
// r98
renderer.render(scene, camera, pickingTexture);
// clear the view offset so rendering returns to normal
camera.clearViewOffset();
//read the pixel
renderer.readRenderTargetPixels(
pickingTexture,
0, // x
0, // y
1, // width
1, // height
pixelBuffer);
const id =
(pixelBuffer[0] << 16) |
(pixelBuffer[1] << 8) |
(pixelBuffer[2]);
return id;
}
}
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;
}
const pickPosition = {
x: 0,
y: 0,
};
const pickHelper = new GPUPickHelper();
let lastPickedId = 0;
let lastPickedObjectSavedEmissive;
function render(time) {
time *= 0.001; // convert to seconds;
if (resizeRendererToDisplaySize(renderer)) {
const canvas = renderer.domElement;
camera.aspect = canvas.clientWidth / canvas.clientHeight;
camera.updateProjectionMatrix();
}
if (lastPickedId) {
pickableObjects[lastPickedId - 1].renderingMaterial.emissive.setHex(lastPickedObjectSavedEmissive);
lastPickedId = 0;
}
for (pickableObject of pickableObjects) {
pickableObject.mesh.material = pickableObject.pickingMaterial;
}
const id = pickHelper.pick(pickPosition, scene, camera, time);
for (pickableObject of pickableObjects) {
pickableObject.mesh.material = pickableObject.renderingMaterial;
}
const pickedObject = pickableObjects[id - 1];
if (pickedObject) {
lastPickedId = id;
lastPickedObjectSavedEmissive = pickedObject.renderingMaterial.emissive.getHex();
pickedObject.renderingMaterial.emissive.setHex((time * 8) % 2 > 1 ? 0xFFFF00 : 0xFF0000);
}
renderer.render(scene, camera);
requestAnimationFrame(render);
};
requestAnimationFrame(render);
function setPickPosition(event) {
pickPosition.x = event.clientX;
pickPosition.y = event.clientY;
}
function clearPickPosition() {
// unlike the mouse which always has a position
// if the user stops touching the screen we want
// to stop picking. For now we just pick a value
// unlikely to pick something
pickPosition.x = -100000;
pickPosition.y = -100000;
}
window.addEventListener('mousemove', setPickPosition);
window.addEventListener('mouseout', clearPickPosition);
window.addEventListener('mouseleave', clearPickPosition);
window.addEventListener('touchstart', (event) => {
// prevent the window from scrolling
event.preventDefault();
setPickPosition(event.touches[0]);
}, {
passive: false
});
window.addEventListener('touchmove', (event) => {
setPickPosition(event.touches[0]);
});
window.addEventListener('touchend', clearPickPosition);
window.addEventListener('mousemove', setPickPosition);
window.addEventListener('mouseout', clearPickPosition);
window.addEventListener('mouseleave', clearPickPosition);
body {
margin: 0;
}
canvas {
width: 100vw;
height: 100vh;
display: block;
}
<script src="https://threejsfundamentals.org/threejs/resources/threejs/r98/three.min.js"></script>
<script src="https://threejsfundamentals.org/threejs/resources/threejs/r98/js/controls/OrbitControls.js"></script>
<canvas></canvas>
The short version: How can one make a camera follow an object controlled by physics within a Three.js scene?
The long version: I'm working on a Three.js scene in which the W,A,S,D keys move a sphere along a plane. So far, however, I haven't figured out how to make the camera follow behind the sphere.
In the example below, the camera follows the sphere perfectly if one only presses the W key. However, if one presses A or D, the sphere starts to turn, and the camera is no longer behind the ball. If the sphere starts to turn, I want the camera to turn with it, so the camera is always following just behind the sphere, and is always a constant distance from the sphere. As users continue to press W, the ball will continue rolling forward relative to the camera.
In a previous scene [demo], I was able to implement this behavior by creating the sphere, adding that sphere to a group, then using the following bit of code each frame:
var relativeCameraOffset = new THREE.Vector3(0,50,200);
var cameraOffset = relativeCameraOffset.applyMatrix4(sphereGroup.matrixWorld);
camera.position.x = cameraOffset.x;
camera.position.y = cameraOffset.y;
camera.position.z = cameraOffset.z;
camera.lookAt(sphereGroup.position);
The key in the demo above was to rotate the sphere while keeping the sphereGroup unrotated, so I could compute the cameraOffset on the un-rotated sphereGroup.
In the demo below, the sphere's position is controlled by the Cannon.js physics library, which translates and rotates the sphere as forces are applied to the body. Does anyone know how I can make the camera follow behind the sphere in the scene below?
/**
* Generate a scene object with a background color
**/
function getScene() {
var scene = new THREE.Scene();
scene.background = new THREE.Color(0x111111);
return scene;
}
/**
* Generate the camera to be used in the scene. Camera args:
* [0] field of view: identifies the portion of the scene
* visible at any time (in degrees)
* [1] aspect ratio: identifies the aspect ratio of the
* scene in width/height
* [2] near clipping plane: objects closer than the near
* clipping plane are culled from the scene
* [3] far clipping plane: objects farther than the far
* clipping plane are culled from the scene
**/
function getCamera() {
var aspectRatio = window.innerWidth / window.innerHeight;
var camera = new THREE.PerspectiveCamera(75, aspectRatio, 0.1, 10000);
camera.position.set(0, 2000, -5000);
camera.lookAt(scene.position);
return camera;
}
/**
* Generate the light to be used in the scene. Light args:
* [0]: Hexadecimal color of the light
* [1]: Numeric value of the light's strength/intensity
* [2]: The distance from the light where the intensity is 0
* #param {obj} scene: the current scene object
**/
function getLight(scene) {
var light = new THREE.PointLight( 0xffffff, 0.6, 0, 0 )
light.position.set( -2000, 1000, -2100 );
scene.add( light );
var light = new THREE.PointLight( 0xffffff, 0.15, 0, 0 )
light.position.set( -190, 275, -1801 );
light.castShadow = true;
scene.add( light );
// create some ambient light for the scene
var ambientLight = new THREE.AmbientLight(0xffffff, 0.8);
scene.add(ambientLight);
return light;
}
/**
* Generate the renderer to be used in the scene
**/
function getRenderer() {
// Create the canvas with a renderer
var renderer = new THREE.WebGLRenderer({antialias: true});
// Add support for retina displays
renderer.setPixelRatio(window.devicePixelRatio);
// Specify the size of the canvas
renderer.setSize(window.innerWidth, window.innerHeight);
// Enable shadows
renderer.shadowMap.enabled = true;
// Specify the shadow type; default = THREE.PCFShadowMap
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
// Add the canvas to the DOM
document.body.appendChild(renderer.domElement);
return renderer;
}
/**
* Generate the controls to be used in the scene
* #param {obj} camera: the three.js camera for the scene
* #param {obj} renderer: the three.js renderer for the scene
**/
function getControls(camera, renderer) {
var controls = new THREE.TrackballControls(camera, renderer.domElement);
controls.zoomSpeed = 0.4;
controls.panSpeed = 0.4;
return controls;
}
/**
* Get stats
**/
function getStats() {
stats = new Stats();
stats.domElement.style.position = 'absolute';
stats.domElement.style.top = '0px';
stats.domElement.style.right = '0px';
document.body.appendChild( stats.domElement );
return stats;
}
/**
* Get grass
**/
function getGrass() {
var texture = loader.load('http://4.bp.blogspot.com/-JiJEc7lH1Is/UHJs3kn261I/AAAAAAAADYA/gQRAxHK2q_w/s1600/tileable_old_school_video_game_grass.jpg');
texture.wrapS = texture.wrapT = THREE.RepeatWrapping;
texture.repeat.set(10, 10);
var material = new THREE.MeshLambertMaterial({
map: texture,
side: THREE.DoubleSide,
});
return material;
}
function getPlanes(scene, loader) {
var planes = [];
var material = getGrass();
[ [4000, 2000, 0, 0, -1000, 0] ].map(function(p) {
var geometry = new THREE.PlaneGeometry(p[0], p[1]);
var plane = new THREE.Mesh(geometry, material);
plane.position.x = p[2];
plane.position.y = p[3];
plane.position.z = p[4];
plane.rotation.y = p[5];
plane.rotation.x = Math.PI / 2;
plane.receiveShadow = true;
planes.push(plane);
scene.add(plane);
})
return planes;
}
/**
* Add background
**/
function getBackground(scene, loader) {
var imagePrefix = 'sky-parts/';
var directions = ['right', 'left', 'top', 'bottom', 'front', 'back'];
var imageSuffix = '.bmp';
var geometry = new THREE.BoxGeometry( 4000, 4000, 4000 );
// Add each of the images for the background cube
var materialArray = [];
for (var i = 0; i < 6; i++)
materialArray.push( new THREE.MeshBasicMaterial({
//map: loader.load(imagePrefix + directions[i] + imageSuffix),
color: 0xff0000,
side: THREE.BackSide
}));
var sky = new THREE.Mesh( geometry, materialArray );
scene.add(sky);
return sky;
}
/**
* Add a character
**/
function getSphere(scene) {
var geometry = new THREE.SphereGeometry( 30, 12, 9 );
var material = new THREE.MeshPhongMaterial({
color: 0xd0901d,
emissive: 0xaa0000,
side: THREE.DoubleSide,
flatShading: true
});
var sphere = new THREE.Mesh( geometry, material );
// allow the sphere to cast a shadow
sphere.castShadow = true;
sphere.receiveShadow = false;
// create a group for translations and rotations
var sphereGroup = new THREE.Group();
sphereGroup.add(sphere)
sphereGroup.castShadow = true;
sphereGroup.receiveShadow = false;
scene.add(sphereGroup);
return [sphere, sphereGroup];
}
/**
* Initialize physics engine
**/
function getPhysics() {
world = new CANNON.World();
world.gravity.set(0, -400, 0); // earth = -9.82 m/s
world.broadphase = new CANNON.NaiveBroadphase();
world.broadphase.useBoundingBoxes = true;
var solver = new CANNON.GSSolver();
solver.iterations = 7;
solver.tolerance = 0.1;
world.solver = solver;
world.quatNormalizeSkip = 0;
world.quatNormalizeFast = false;
world.defaultContactMaterial.contactEquationStiffness = 1e9;
world.defaultContactMaterial.contactEquationRelaxation = 4;
return world;
}
/**
* Generate the materials to be used for contacts
**/
function getPhysicsMaterial() {
var physicsMaterial = new CANNON.Material('slipperyMaterial');
var physicsContactMaterial = new CANNON.ContactMaterial(
physicsMaterial, physicsMaterial, 0.0, 0.3)
world.addContactMaterial(physicsContactMaterial);
return physicsMaterial;
}
/**
* Add objects to the world
**/
function addObjectPhysics() {
addFloorPhysics()
addSpherePhysics()
}
function addFloorPhysics() {
floors.map(function(floor) {
var q = floor.quaternion;
floorBody = new CANNON.Body({
mass: 0, // mass = 0 makes the body static
material: physicsMaterial,
shape: new CANNON.Plane(),
quaternion: new CANNON.Quaternion(-q._x, q._y, q._z, q._w)
});
world.addBody(floorBody);
})
}
function addSpherePhysics() {
sphereBody = new CANNON.Body({
mass: 1,
material: physicsMaterial,
shape: new CANNON.Sphere(30),
linearDamping: 0.5,
position: new CANNON.Vec3(1000, 500, -2000)
});
world.addBody(sphereBody);
}
/**
* Store all currently pressed keys & handle window resize
**/
function addListeners() {
window.addEventListener('keydown', function(e) {
pressed[e.key.toUpperCase()] = true;
})
window.addEventListener('keyup', function(e) {
pressed[e.key.toUpperCase()] = false;
})
window.addEventListener('resize', function(e) {
windowHalfX = window.innerWidth / 2;
windowHalfY = window.innerHeight / 2;
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
if (typeof(controls) != 'undefined') controls.handleResize();
})
}
/**
* Update the sphere's position
**/
function moveSphere() {
var delta = clock.getDelta(); // seconds
var moveDistance = 500 * delta; // n pixels per second
var rotateAngle = Math.PI / 2 * delta; // 90 deg per second
// move forwards, backwards, left, or right
if (pressed['W'] || pressed['ARROWUP']) {
sphereBody.velocity.z += moveDistance;
}
if (pressed['S'] || pressed['ARROWDOWN']) {
sphereBody.velocity.z -= moveDistance;
}
if (pressed['A'] || pressed['ARROWLEFT']) {
sphereBody.velocity.x += moveDistance;
}
if (pressed['D'] || pressed['ARROWRIGHT']) {
sphereBody.velocity.x -= moveDistance;
}
}
/**
* Follow the sphere
**/
function moveCamera() {
camera.position.x = sphereBody.position.x + 0;
camera.position.y = sphereBody.position.y + 50;
camera.position.z = sphereBody.position.z + -200;
camera.lookAt(sphereGroup.position);
}
function updatePhysics() {
world.step(1/60);
sphereGroup.position.copy(sphereBody.position);
sphereGroup.quaternion.copy(sphereBody.quaternion);
}
// Render loop
function render() {
requestAnimationFrame(render);
renderer.render(scene, camera);
moveSphere();
updatePhysics();
if (typeof(controls) === 'undefined') moveCamera();
if (typeof(controls) !== 'undefined') controls.update();
if (typeof(stats) !== 'undefined') stats.update();
};
// state
var pressed = {};
var clock = new THREE.Clock();
// globals
var scene = getScene();
var camera = getCamera();
var light = getLight(scene);
var renderer = getRenderer();
var world = getPhysics();
var physicsMaterial = getPhysicsMaterial();
//var stats = getStats();
//var controls = getControls(camera, renderer);
// global body references
var sphereBody, floorBody;
// add meshes
var loader = new THREE.TextureLoader();
var floors = getPlanes(scene, loader);
var background = getBackground(scene, loader);
var sphereData = getSphere(scene);
var sphere = sphereData[0];
var sphereGroup = sphereData[1];
addObjectPhysics();
addListeners();
render();
body { margin: 0; overflow: hidden; }
canvas { width: 100%; height: 100%; }
<script src='https://cdnjs.cloudflare.com/ajax/libs/three.js/88/three.min.js'></script>
<script src='https://cdnjs.cloudflare.com/ajax/libs/cannon.js/0.6.2/cannon.js'></script>
Answers to comment questions
#jparimaa I think the most intuitive implementation would make W add forward momentum, S add backward momentum, and A and D rotate the camera around the ball. Is that possible?
#HariV The controls you link to are the ones I used in the demo without physics above. Is it possible to get that logic working with physics?
I think it's most intuitive for users if the W key always moves the ball "forward" relative to the camera
One option would be to calculate the direction between the ball and the camera and add velocity to that direction. In this case if you push the ball forward then you could rotate the camera without it affecting the velocity of the ball. Only after you press W/S after the rotation it would change the direction. I'm not sure if that is what you want but maybe this will give you some ideas.
I tried the following code (rotation is global variable initialized to 0)
function moveSphere() {
var delta = clock.getDelta(); // seconds
var moveDistance = 500 * delta; // n pixels per second
var dir = new THREE.Vector3(sphereBody.position.x, sphereBody.position.y, sphereBody.position.z);
dir.sub(camera.position).normalize(); // direction vector between the camera and the ball
if (pressed['W'] || pressed['ARROWUP']) {
sphereBody.velocity.x += moveDistance * dir.x;
sphereBody.velocity.z += moveDistance * dir.z;
}
if (pressed['S'] || pressed['ARROWDOWN']) {
sphereBody.velocity.x -= moveDistance * dir.x;
sphereBody.velocity.z -= moveDistance * dir.z;
}
}
function moveCamera() {
var delta = clock.getDelta();
var sensitivity = 150;
var rotateAngle = Math.PI / 2 * delta * sensitivity;
if (pressed['A'] || pressed['ARROWLEFT']) {
rotation -= rotateAngle;
}
if (pressed['D'] || pressed['ARROWRIGHT']) {
rotation += rotateAngle;
}
var rotZ = Math.cos(rotation)
var rotX = Math.sin(rotation)
var distance = 200;
camera.position.x = sphereBody.position.x - (distance * rotX);
camera.position.y = sphereBody.position.y + 50;
camera.position.z = sphereBody.position.z - (distance * rotZ);
camera.lookAt(sphereGroup.position);
}
I'm trying to rotate multiple objects around the same Vector3 point but in different directions - so they effectively 'swarm' around the point.
I'm using the trig approach rather simply wrapping each object in a Container and applying some random rotations as I'm projecting their 3D vector to 2D positions to attach label DIVs above the canvas, and the container approach messes with with project class.
Here's my current code which makes all objects rotate around the point along the same orbit path:
for(var i = 0; i<objectsArr.length; i++){
var obj = objectsArr[i];
var radius = obj.angle * (Math.PI / 180);
obj.position.x = obj.radius * Math.cos(radius);
obj.position.y = obj.radius * Math.sin(radius);
obj.angle += obj.orbitSpeed;
}
Does anyone know how I can make them orbit in random directions along X, Y, Z axis?
First, see this answer about rotating objects about a point.
From your code, it looks like you're storing each object's orbit speed, and their current angle of rotation (which represents the vector to the position). Rather than storing a 2D angle, store it as a Vector3 which represents the normal of (perpendicular to) the object's orbital plane. (This will let you really get a "swarm" going later, when you can define different orbital planes.) I also recommend storing the orbit speed in radians per step, so you don't have to perform the conversion every pass.
The rest of problem actually becomes very simple with the Vector3.applyAxisAngle method.
Some pseudo-code:
Subtract the rotation point position from the object's position.
Use the object's orbit speed, and angle to update the temp position.
Add the rotation point position back to the object's position.
To see it in your code:
var obj;
for(var i = 0; i< objectsArr.length; i++){
obj = objectsArr[i];
obj.position.sub(rotationPoint); // rotationPoint is a Vector3
obj.position.applyAxisAngle(obj.angle, obj.orbitSpeed);
obj.add(rotationPoint);
}
And here's a live demo of a few objects orbiting randomly about a "nucleus" at (10, 10, 10).
var renderer, scene, camera, controls, stats, nucleus;
var WIDTH = window.innerWidth,
HEIGHT = window.innerHeight,
FOV = 60,
NEAR = 1,
FAR = 1000;
var electrons = [],
numElectrons = 100; // more electrons = slower updating
function populateScene() {
var geo = new THREE.SphereBufferGeometry(10, 16, 16);
var mat = new THREE.MeshPhongMaterial({color:"blue"});
nucleus = new THREE.Mesh(geo, mat);
nucleus.position.set(10, 10, 10); // you can change these values
scene.add(nucleus);
var electron = null,
plane = new THREE.Plane(),
point = new THREE.Vector3();
geo = new THREE.SphereBufferGeometry(1, 16, 16);
mat = new THREE.MeshPhongMaterial({color:"red"});
for(var i = 0; i < numElectrons; ++i){
electron = new THREE.Mesh(geo, mat);
electrons.push(electron);
electron.angle = new THREE.Vector3(
Math.random(),
Math.random(),
Math.random()
).normalize();
electron.orbitSpeed = (Math.random() * 0.05) + 0.05;
if(Math.random() > 0.5) electron.orbitSpeed *= -1;
plane.normal.copy(electron.angle);
point.set(Math.random(), Math.random(), Math.random());
plane.projectPoint(point, electron.position);
electron.position.setLength(Math.floor(Math.random() * 20) + 15);
electron.position.applyAxisAngle(electron.angle, Math.random() / 10);
electron.position.add(nucleus.position);
scene.add(electron);
}
}
function updateElectrons(){
var obj = null;
for(var i = 0; i < numElectrons; ++i){
obj = electrons[i]
obj.position.sub(nucleus.position);
obj.position.applyAxisAngle(obj.angle, obj.orbitSpeed);
obj.position.add(nucleus.position);
}
}
function init() {
document.body.style.backgroundColor = "slateGray";
renderer = new THREE.WebGLRenderer({
antialias: true,
alpha: true
});
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
document.body.appendChild(renderer.domElement);
document.body.style.overflow = "hidden";
document.body.style.margin = "0";
document.body.style.padding = "0";
scene = new THREE.Scene();
camera = new THREE.PerspectiveCamera(FOV, WIDTH / HEIGHT, NEAR, FAR);
camera.position.z = 100;
scene.add(camera);
controls = new THREE.TrackballControls(camera, renderer.domElement);
controls.dynamicDampingFactor = 0.5;
controls.rotateSpeed = 3;
var light = new THREE.PointLight(0xffffff, 1, Infinity);
camera.add(light);
stats = new Stats();
stats.domElement.style.position = 'absolute';
stats.domElement.style.top = '0';
document.body.appendChild(stats.domElement);
resize();
window.onresize = resize;
populateScene();
animate();
}
function resize() {
WIDTH = window.innerWidth;
HEIGHT = window.innerHeight;
if (renderer && camera && controls) {
renderer.setSize(WIDTH, HEIGHT);
camera.aspect = WIDTH / HEIGHT;
camera.updateProjectionMatrix();
controls.handleResize();
}
}
function render() {
renderer.render(scene, camera);
}
function animate() {
requestAnimationFrame(animate);
updateElectrons();
render();
controls.update();
stats.update();
}
function threeReady() {
init();
}
(function() {
function addScript(url, callback) {
callback = callback || function() {};
var script = document.createElement("script");
script.addEventListener("load", callback);
script.setAttribute("src", url);
document.head.appendChild(script);
}
addScript("https://threejs.org/build/three.js", function() {
addScript("https://threejs.org/examples/js/controls/TrackballControls.js", function() {
addScript("https://threejs.org/examples/js/libs/stats.min.js", function() {
threeReady();
})
})
})
})();
three.js r86
I have a panoramic view and want to blur it in order to present a user-interface in front of the blurred view. The blurred image has to be computed on the client so I decided to use a fragment-shader-based implementation to do that.
This works very nice as long as I only use only the regular renderer.
But when the scene is rendered using the THREE.StereoEffect instead, the blurred image doesn't appear on screen.
You can see this in the attached snippet (jsfiddle here: https://jsfiddle.net/n988sg96/3/): If you press "toggle blur" everything looks like it should. But if you press "toggle stereo" and then activate the blur, the screen just turns black (so basically, the blurred image will not render).
The generation of the blurred image is implemented in createBlurredTexture() using the same renderer that is also used for the scene and two render-targets for the vertical and horizontal pass of the blur.
I already verified (by exporting the framebuffers as image via renderer.readRenderTargetPixels()) that both render-targets contain correct images in both cases (so independent of wether the stereo-mode is on or not).
So the questions I have are:
why is it that the texture from the RenderTarget is not rendered with the StereoEffect?
are there other comparable options to achieve the same effect?
const panoUrl = 'https://farm9.staticflickr.com/8652/29593302665_9e747048f7_k_d.jpg';
const panoTexture = new THREE.Texture();
const image = new Image();
image.crossOrigin = 'Anonymous';
image.onload = () => {
panoTexture.image = image;
panoTexture.format = THREE.RGBFormat;
panoTexture.needsUpdate = true;
};
image.src = panoUrl;
const blurButton = document.querySelector('.blur-btn');
const stereoButton = document.querySelector('.stereo-btn');
// creates meshes
function initScene(scene, renderer) {
const panoSphere = new THREE.Mesh(
new THREE.SphereGeometry(100, 36, 18),
new THREE.MeshBasicMaterial({
depthWrite: false,
map: panoTexture
}));
const blurSphere = new THREE.Mesh(
new THREE.SphereGeometry(80, 36, 18),
new THREE.MeshBasicMaterial({
color: 0x666666
})
);
// flip normals
blurSphere.scale.x = panoSphere.scale.x = -1;
blurSphere.visible = false;
scene.add(panoSphere, blurSphere);
blurButton.addEventListener('click', ev => {
if (blurSphere.visible) {
blurSphere.visible = false;
} else {
blurSphere.material.map = createBlurredTexture(
renderer, panoSphere.material.map.image);
blurSphere.material.needsUpdate = true;
blurSphere.visible = true;
}
});
}
// creates a blurred image-texture from the given image
function createBlurredTexture(renderer, img, prescale = 0.25) {
const width = img.width * prescale;
const height = img.height * prescale;
const material = blurPassMaterial;
const camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);
const scene = new THREE.Scene()
.add(new THREE.Mesh(new THREE.PlaneBufferGeometry(2, 2), material));
const renderTargetOpts = {
depthBuffer: false,
stencilBuffer: false
};
const rt1 = new THREE.WebGLRenderTarget(width, height, renderTargetOpts);
const rt2 = new THREE.WebGLRenderTarget(width, height, renderTargetOpts);
material.uniforms.resolution.value.set(width, height);
// prepare: downscale source-image
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
canvas.getContext('2d').drawImage(img, 0, 0, width, height);
const texture = new THREE.CanvasTexture(canvas);
texture.wrapS = texture.wrapT = THREE.RepeatWrapping;
// pass 1: vertical blur, texture -> rt1
material.uniforms.image.value = texture;
material.uniforms.direction.value.set(0, 1);
renderer.render(scene, camera, rt1);
// pass 2: horizontal blur, rt1 -> rt2
material.uniforms.image.value = rt1.texture;
material.uniforms.direction.value.set(1, 0);
renderer.render(scene, camera, rt2);
// cleanup
texture.dispose();
rt1.texture.dispose();
rt1.dispose();
return rt2.texture;
}
// simple material for a fast 5px blur pass
const blurPassMaterial = new THREE.ShaderMaterial({
uniforms: {
image: {type: 't', value: null},
resolution: {type: 'v2', value: new THREE.Vector2()},
direction: {type: 'v2', value: new THREE.Vector2(1, 0)}
},
vertexShader: `
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`,
fragmentShader: `
varying vec2 vUv;
uniform vec2 direction;
uniform vec2 resolution;
uniform sampler2D image;
// based on https://github.com/Jam3/glsl-fast-gaussian-blur
vec4 blur5(sampler2D image, vec2 uv, vec2 resolution, vec2 direction) {
vec2 offset = (vec2(1.3333333333333333) * direction) / resolution;
return texture2D(image, uv) * 0.29411764705882354
+ texture2D(image, uv + offset) * 0.35294117647058826
+ texture2D(image, uv - offset) * 0.35294117647058826;
}
void main() {
gl_FragColor = blur5(image, vUv, resolution, direction);
}
`
});
// ---- boilerplate-code
// .... setup renderer and stereo-effect
let isStereoMode = false;
const renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
const effect = new THREE.StereoEffect(renderer);
// .... setup scene
const scene = window.scene = new THREE.Scene();
// .... setup camera and controls
const camera = new THREE.PerspectiveCamera(
70, window.innerWidth / window.innerHeight, 1, 1000);
const controls = new THREE.OrbitControls(camera);
controls.enableZoom = false;
controls.enableDamping = true;
controls.dampingFactor = .15;
camera.position.set(0, 0, .1);
camera.lookAt(new THREE.Vector3(0, 0, 0));
// .... setup and run
initScene(scene, renderer);
requestAnimationFrame(function loop(time) {
controls.update();
if (isStereoMode) {
effect.render(scene, camera);
} else {
renderer.render(scene, camera);
}
requestAnimationFrame(loop);
});
// .... bind events
stereoButton.addEventListener('click', ev => {
isStereoMode = !isStereoMode;
if (!isStereoMode) {
renderer.setViewport(0, 0, window.innerWidth, window.innerHeight);
}
});
window.addEventListener('resize', ev => {
renderer.setSize(window.innerWidth, window.innerHeight);
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
});
document.body.appendChild(renderer.domElement);
body {
margin: 0;
overflow: hidden;
}
canvas {
width: 100vw;
height: 100vh;
}
.buttons {
position: absolute;
top: 10px;
left: 0;
right: 0;
text-align: center;
}
button {
display: inline-block;
}
<script src="https://cdn.rawgit.com/mrdoob/three.js/master/build/three.js"></script>
<script src="https://cdn.rawgit.com/mrdoob/three.js/master/examples/js/controls/OrbitControls.js"></script>
<script src="https://cdn.rawgit.com/mrdoob/three.js/master/examples/js/effects/StereoEffect.js"></script>
<div class="buttons">
<button class="blur-btn">toggle blur</button>
<button class="stereo-btn">toggle stereo</button>
</div>
Found a solution reading this Question: three.js - THREE.StereoEffect / webVR-boilerplate + THREE.Mirror
And I just needed to add a single line to the createBlurredTexture()-function.
When cleaning up, it is neccessary to manually unset the renderTarget by calling
renderer.setRenderTarget(null);
The reason for this is that the rendering of the stereo-effect will call renderer.clear(), which will - without unsetting the rendertarget - clear the renderTarget instead of the screen framebuffer.
So thanks a lot stackoverflow <3