I'm trying to smoothly scale cubes with random initial scaling. The problem is that after some short time cubes scale almost identically, besides I'm using random noise function for scale. What's wrong with my code?
Preview is here https://twilight-sweatshirt.glitch.me/
Live snippet https://stackblitz.com/edit/react-gakauv
As you can see cubes scaling becomes almost identical very fast.
Here's my code
const random = require("canvas-sketch-util/random");
const palettes = require("nice-color-palettes");
random.setSeed(19);
const palette = random.pick(palettes);
// Ensure ThreeJS is in global scope for the 'examples/'
global.THREE = require("three");
// Include any additional ThreeJS examples below
require("three/examples/js/controls/OrbitControls");
const canvasSketch = require("canvas-sketch");
const settings = {
// Make the loop animated
animate: true,
// duration: 5,
fps: 30,
playbackRate: "throttle",
// Get a WebGL canvas rather than 2D
context: "webgl",
attributes: {
antialias: true
}
};
const sketch = ({ context }) => {
//#region Scene setup
// Create a renderer
const renderer = new THREE.WebGLRenderer({
canvas: context.canvas
});
renderer.setClearColor("aquamarine", 1);
const camera = new THREE.OrthographicCamera();
const controls = new THREE.OrbitControls(camera, context.canvas);
const scene = new THREE.Scene();
scene.add(new THREE.AmbientLight(0x404040));
const light = new THREE.PointLight(0xffffff, 5, 15);
light.position.set(-1, 2, 4).multiplyScalar(1.5);
scene.add(light);
const light2 = new THREE.PointLight(0xffffff, 1.5, 15);
light2.position.set(3, 0, 2).multiplyScalar(1.5);
scene.add(light2);
const geometry = new THREE.BoxGeometry(1, 1, 1);
const getNoise = (x, time) => {
return random.noise2D(x, time, 0.5) * 0.5 + 0.5;
};
for (let index = 0; index < 2; index++) {
const mesh = new THREE.Mesh(
geometry,
new THREE.MeshStandardMaterial({
color: random.pick(palette),
flatShading: true,
roughness: 0.75
})
);
mesh.name = `mesh-${index}`;
mesh.position.set(
random.range(-1, 1),
random.range(-1, 1),
random.range(-1, 1)
);
mesh.scale.set(
random.range(0.04, 0.5),
random.range(0.04, 0.5),
random.range(0.04, 0.5)
);
scene.add(mesh);
}
//#endregion
// draw each frame
return {
// Handle resize events here
resize({ pixelRatio, viewportWidth, viewportHeight }) {
renderer.setPixelRatio(pixelRatio);
renderer.setSize(viewportWidth, viewportHeight, false);
const aspect = viewportWidth / viewportHeight;
// Ortho zoom
const zoom = 1.5;
// Bounds
camera.left = -zoom * aspect;
camera.right = zoom * aspect;
camera.top = zoom;
camera.bottom = -zoom;
// Near/Far
camera.near = -100;
camera.far = 100;
// Set position & look at world center
camera.position.set(zoom, zoom, zoom);
camera.lookAt(new THREE.Vector3());
// Update the camera
camera.updateProjectionMatrix();
},
// Update & render your scene here
render({ time, playhead }) {
scene.children.forEach(obj => {
if (obj.isMesh !== true) {
return;
}
// console.log(`${obj.name}: `,getNoise(obj.scale.z, time));
// console.log(scene);
obj.scale.set(
// obj.scale.x,
// obj.scale.y,
// obj.scale.z,
getNoise(obj.scale.x, time),
getNoise(obj.scale.y, time),
getNoise(obj.scale.z, time)
);
});
controls.update();
renderer.render(scene, camera);
},
// Dispose of events & renderer for cleaner hot-reloading
unload() {
controls.dispose();
renderer.dispose();
}
};
};
canvasSketch(sketch, settings);
For the maximum calculation, the noise difference was much so it was flickering the box.
Try Setting these parameters
return random.noise2D(x, time, 0.8) * 0.2 + 0.5;
OR
return random.noise2D(x, time, 0.7) * 0.3 + 0.5;
OR
return random.noise2D(x, time, 0.6) * 0.4 + 0.5;
Choose as per your frequency.
See, if this is what your were looking for https://stackblitz.com/edit/react-hhurr3?file=index.js
Related
So I am using gsap and three js my aim is to animate a light
I have an array of colors i wish to use i have code set up so 1 of these colors are picked randomly
now i can get it to pick random colors and repeat infinity ... this is what I want .....
now when I use onCompleteAll: change, this works all good and fine BUT it doesn't animate the colors it just "jumps" to the next color when the duration is done so no animation it just changes straight away when the time is up
so I thought Oh well easy enough to fix i will use onComplete: change, instead now this never fires tho? so what ends up happening is the colors change and animate perfectly but only 2 of the colors from the array because the change(); function never gets fired so either does the new random value
Math.floor(Math.random() * std.length)
can anyone spot any obvious mistakes or know of any fixes?
here is my code:
var std = [new THREE.Color().setHex(0x009dff),
new THREE.Color().setHex(0x001aff),
new THREE.Color().setHex(0x4000ff),
new THREE.Color().setHex(0x7300ff)];
var randomIndex, randomColor, tempColor, camlight3;
randomIndex = Math.floor(Math.random() * std.length);
randomColor = std[randomIndex];
tempColor = randomColor;
// Three.js camera
/* camlight3 = new THREE.PointLight(tempColor, 60, 80, 0.0);
camlight3.power = 60;
camlight3.position.x += 10;
camlight3.position.y += 25;
camlight3.position.z -= 120;
this._camera.add(camlight3); */
gsap.to(camlight3.color,
{
duration: 2,
r: tempColor.r, g: tempColor.g, b: tempColor.b,
onCompleteAll: change,
yoyo: true,
repeat: -1,
repeatRefresh: true,
});
function change() {
randomIndex = Math.floor(Math.random() * std.length);
randomColor = std[randomIndex];
tempColor = randomColor;
camlight3.color = tempColor;
console.log(tempColor);
}
thanks for any help
EDIT i should mention I have other animations doing on that use the oncomplete and onupdate if that matters non of the other though call change or are associated with the light
I made it working with calling of tweening recursively (not sure how correct this approach for GSAP):
body{
overflow: hidden;
margin: 0;
}
<script type="module">
import * as THREE from "https://cdn.skypack.dev/three#0.136.0";
import { OrbitControls } from "https://cdn.skypack.dev/three#0.136.0/examples/jsm/controls/OrbitControls";
import {gsap} from "https://cdn.skypack.dev/gsap#3.11.2";
console.clear();
let scene = new THREE.Scene();
let camera = new THREE.PerspectiveCamera(60, innerWidth / innerHeight, 1, 1000);
camera.position.set(0, 10, 10);
let renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(innerWidth, innerHeight);
document.body.appendChild(renderer.domElement);
window.addEventListener("resize", (event) => {
camera.aspect = innerWidth / innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(innerWidth, innerHeight);
});
let controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.enablePan = false;
let floor = new THREE.Mesh(
new THREE.PlaneGeometry(10, 10).rotateX(-Math.PI * 0.5),
new THREE.MeshStandardMaterial({ color: 0xffffff })
);
scene.add(floor);
let colors = [0xff00ff, 0x00ffff, 0xffff00, 0xff0000, 0x00ff00, 0x0000ff];
let getColor = () => {return colors[Math.floor(Math.random() * colors.length)]};
let light = new THREE.SpotLight(getColor(), 0.5, 0, Math.PI * 0.1, 0.5);
let nextColor = new THREE.Color(getColor());
light.position.set(5, 7, 0);
scene.add(light, new THREE.AmbientLight(0xffffff, 0.5));
let lightHelper = new THREE.SpotLightHelper(light);
scene.add(lightHelper);
(function runSequence(){
gsap.to(light.color, {
r: nextColor.r,
g: nextColor.g,
b: nextColor.b,
duration: 0.5,
onComplete: function(){
nextColor.set(getColor());
runSequence();
}
});
})(); // random colors
let clock = new THREE.Clock();
renderer.setAnimationLoop(() => {
let t = clock.getElapsedTime();
controls.update();
light.position.x = Math.sin(t * 0.25) * 5;
lightHelper.update();
renderer.render(scene, camera);
});
</script>
Goal
I am trying to create an EllipseCurve (https://threejs.org/docs/#api/en/extras/curves/EllipseCurve) on which a camera should move.
What I did to achive the goal?
This is the code for the ellipse so far.
var curve = new THREE.EllipseCurve(
0,0,
1, 1,
0, 2 * Math.PI,
false,
1.57
)
const points = curve.getPoints( 50 );
const geometry = new THREE.BufferGeometry().setFromPoints( points );
var material = new THREE.LineBasicMaterial( { color : 0xffffff } );
// Create the final object to add to the scene
var curveObject = new THREE.Line( geometry, material );
scene.add(curveObject);
I can see it in the scene like this:
The Problem
I tried to rotate the Ellipse Curve 90 degree arround the x axis clockwise. As I understood from the documentation, the last parameter of the defining function should rotate it.
const curve = new THREE.EllipseCurve(
0, 0, // ax, aY
10, 10, // xRadius, yRadius
0, 2 * Math.PI, // aStartAngle, aEndAngle
false, // aClockwise
0 // aRotation
);
Thank you in advance for you answer. I am quite new to Three.js so sorry if this question might be stupid :D
Get a point on the curve and apply a matrix4 to it.
Here is a concept of how you can do it (see the lines with cam in the animation loop, better to watch with "Full page"):
body{
overflow: hidden;
margin: 0;
}
<script type="module">
import * as THREE from "https://cdn.skypack.dev/three#0.134.0";
import {
OrbitControls
} from "https://cdn.skypack.dev/three#0.134.0/examples/jsm/controls/OrbitControls.js";
let scene = new THREE.Scene();
let camera = new THREE.PerspectiveCamera(60, innerWidth / innerHeight, 1, 1000);
camera.position.set(-10, 10, 10);
let renderer = new THREE.WebGLRenderer({
antialias: true
});
renderer.setSize(innerWidth, innerHeight);
renderer.autoClear = false;
document.body.appendChild(renderer.domElement);
window.addEventListener("resize", () => {
camera.aspect = innerWidth / innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(innerWidth, innerHeight);
})
let controls = new OrbitControls(camera, renderer.domElement);
let light = new THREE.DirectionalLight(0xffffff, 1);
light.position.setScalar(1);
scene.add(light, new THREE.AmbientLight(0xffffff, 0.5));
let grid = new THREE.GridHelper();
grid.position.y = -5;
scene.add(grid);
let obj = new THREE.Mesh(new THREE.IcosahedronGeometry(1, 0), new THREE.MeshLambertMaterial({
color: "aqua"
}));
scene.add(obj);
let curve = new THREE.EllipseCurve(0, 0, 10, 5);
let line = new THREE.Line(new THREE.BufferGeometry().setFromPoints(curve.getSpacedPoints(100)), new THREE.LineBasicMaterial({
color: "yellow"
}));
line.rotation.x = -Math.PI * 0.25;
line.rotation.z = Math.PI * 0.125;
line.position.x = 5;
scene.add(line);
let cam = new THREE.PerspectiveCamera(25, 1, 1.5, 25);
let camHelper = new THREE.CameraHelper(cam);
scene.add(camHelper);
let clock = new THREE.Clock();
let v = new THREE.Vector3();
renderer.setAnimationLoop(() => {
let t = (clock.getElapsedTime() * 0.05) % 1;
// magic is in these lines //////////////////
cam.position.copy(curve.getPointAt(t, v));
cam.position.applyMatrix4(line.matrixWorld);
cam.lookAt(obj.position);
/////////////////////////////////////////////
renderer.clear();
renderer.setViewport(0, 0, innerWidth, innerHeight);
renderer.render(scene, camera);
renderer.setViewport(0, innerHeight - 256, 256, 256);
renderer.render(scene, cam);
})
</script>
the aRotation, angle-rotation, will affect the local origin of other angle settings for this curve. It is not the overall ellipse rotation, but the orientation of any offset relative to default. A different starting point. It would turn a Pac-Man mouth into a backwards Pac-Man mouth at +/- 180-degrees. To rotate the overall curve in world-space, use one of the various methods available, such as curve.rotation.set(0,1,0) or rotation.y += 1. Please refer to documentation for specific variants of rotation.
I have code in place to create load, render and display a STL object using Vue.js and Three.js.
I want to render a new face in place of the plane I am currently racasting to. I have already found a way to get the 3 vertices of the mous-overed (Clicked on) face (aVertex, bVertex, cVertex).
Now I want to render a triangle in this position (with a different color), but to be honest I have no idea how. I have tried googling it, but with no luck yet (I am very unfamiliar with 3d and rendering in general). Can someone give me a push in the right direction?
<template>
<div id="scene-container" ref="sceneContainer" class="scene-container"></div>
</template>
<script>
import * as THREE from "three";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
import { STLLoader } from "three/examples/jsm/loaders/STLLoader";
export default {
name: "HelloWorld",
data() {
return {
container: null,
scene: null,
camera: null,
controls: null,
renderer: null,
stats: null,
mouse: null,
raycaster: null,
objName: "testobj",
};
},
methods: {
init() {
// set container
this.container = this.$refs.sceneContainer;
// add raycaster
this.raycaster = new THREE.Raycaster();
this.mouse = new THREE.Vector2();
const onMouseClick = (event) => {
event.preventDefault();
// calculate mouse position in normalized device coordinates
// (-1 to +1) for both components
this.mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
this.mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
// update the picking ray with the camera and mouse position
};
window.addEventListener("mousedown", onMouseClick, false);
// add camera
const fov = 60; // Field of view
const aspect = this.container.clientWidth / this.container.clientHeight;
// const near = 0.1; // the near clipping plane
// const far = 3000; // the far clipping plane
const camera = new THREE.PerspectiveCamera(fov, aspect);
camera.position.set(0, 20, 75);
this.camera = camera;
// create scene
this.scene = new THREE.Scene();
this.scene.background = new THREE.Color(0xdddddd);
// add lights
const ambientLight = new THREE.HemisphereLight(
0xffffff, // bright sky color
0x222222, // dim ground color
1 // intensity
);
const mainLight = new THREE.DirectionalLight(0xffffff, 4.0);
mainLight.position.set(10, 10, 10);
this.scene.add(ambientLight, mainLight);
let hlight = new THREE.AmbientLight(0xffffff, 1.3);
this.scene.add(hlight);
//Add some point lights to simulate real lights
let light = new THREE.PointLight(0xffffff, 1, 1000);
light.position.set(0, 300, 500);
this.scene.add(light);
// add controls
this.controls = new OrbitControls(this.camera, this.container);
// create renderer
this.renderer = new THREE.WebGLRenderer({ antialias: true });
this.renderer.setSize(
this.container.clientWidth,
this.container.clientHeight
);
this.renderer.setPixelRatio(window.devicePixelRatio);
this.renderer.gammaFactor = 2.2;
this.renderer.outputEncoding = THREE.sRGBEncoding;
this.renderer.physicallyCorrectLights = true;
document
.getElementById("scene-container")
.appendChild(this.renderer.domElement);
// set aspect ratio to match the new browser window aspect ratio
this.camera.aspect =
this.container.clientWidth / this.container.clientHeight;
this.camera.updateProjectionMatrix();
this.renderer.setSize(
this.container.clientWidth,
this.container.clientHeight
);
let me = this;
let loader = new STLLoader();
let mesh = new THREE.Object3D();
loader.load("/three-assets/RobotExpressive.stl", function (geometry) {
// console.log(geometry);
let material = new THREE.MeshLambertMaterial({
color: 0x1313,
wireframe: false,
transparent: false,
vertexColors: false,
});
mesh = new THREE.Mesh(geometry, material);
mesh.rotation.x = -0.5 * Math.PI;
mesh.position.set(0, 0, 0);
mesh.name = me.objName;
me.scene.add(mesh);
});
window.addEventListener("resize", onWindowResize, false);
function onWindowResize() {
me.camera.aspect = window.innerWidth / window.innerHeight;
me.camera.updateProjectionMatrix();
me.renderer.setSize(window.innerWidth, window.innerHeight);
}
this.renderer.setAnimationLoop(() => {
this.render();
});
},
render() {
this.raycaster.setFromCamera(this.mouse, this.camera);
this.intersects = this.raycaster.intersectObjects(this.scene.children);
// window.addEventListener( 'mousemove', onMouseMove, false );
if (this.intersects.length > 1) {
// this.intersects[0].object.material.color.set(0xff);
for (let i = 0; i < this.intersects.length; i++) {
if (this.intersects[i].object.name == "testobj") {
let positionAttribute = this.intersects[i].object.geometry
.attributes["position"];
let intersection = this.intersects[i];
let aVertex = new THREE.Vector3(
positionAttribute.getX(intersection.face.a),
positionAttribute.getY(intersection.face.a),
positionAttribute.getZ(intersection.face.a)
);
let bVertex = new THREE.Vector3(
positionAttribute.getX(intersection.face.b),
positionAttribute.getY(intersection.face.b),
positionAttribute.getZ(intersection.face.b)
);
let cVertex = new THREE.Vector3(
positionAttribute.getX(intersection.face.c),
positionAttribute.getY(intersection.face.c),
positionAttribute.getZ(intersection.face.c)
);
console.log(aVertex, bVertex, cVertex);
}
}
}
this.renderer.render(this.scene, this.camera);
},
},
mounted() {
this.init();
},
};
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
h3 {
margin: 40px 0 0;
}
ul {
list-style-type: none;
padding: 0;
}
li {
display: inline-block;
margin: 0 10px;
}
a {
color: #42b983;
}
#scene-container {
height: 99.8%;
}
</style>
Example (for picking meshes with BufferGeometry):
// initital setup:
let lineGeometry = new THREE.BufferGeometry();
let linePositionAttribute = new THREE.BufferAttribute(new Float32Array(4 * 3), 3);
lineGeometry.addAttribute('position', linePositionAttribute);
let lineMaterial = new THREE.LineBasicMaterial(
{
color: 0xff0000
});
var intersectionFaceEdge = new THREE.Line(lineGeometry, lineMaterial);
scene.add(intersectionFaceEdge);
// ... on each raycasting:
let face = intersection.face;
let obj = intersection.object;
let positionAttribute = obj.geometry.attributes['position'];
linePositionAttribute.copyAt(0, positionAttribute, face.a);
linePositionAttribute.copyAt(1, positionAttribute, face.b);
linePositionAttribute.copyAt(2, positionAttribute, face.c);
linePositionAttribute.copyAt(3, positionAttribute, face.a);
lineGeometry.applyMatrix(obj.matrixWorld);
And I recommend using GPU picking instead of simple raycasting
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);
}