I am trying to achieve polygon blowing and reassembling effect similar to:
- http://na.leagueoflegends.com/en/featured/skins/project-2016
- https://tkmh.me/
In both of these examples, you can see how they are morphing / transitioning the vertices from one 3d model to another, resulting in a pretty cool effect. I have something similar working, but I can't wrap my head around how they are transitioning the vertices with velocity offsets (please refer to the first link and see how the particles don't simply map and ease to the new position, but rather do it with some angular offset):
So, I import my two models in Three.js, take the one that has bigger vert count and copy its geometry while attaching the second's model data as an attribute:
class CustomGeometry extends THREE.BufferGeometry {
constructor (geometry1, geometry2) {
super()
let { count } = geometry1.attributes.position
// this will hold
let targetArr = new Float32Array(count * 3)
let morphFactorArr = new Float32Array(count)
for (let i = 0; i < count; i += 3) {
targetArr[i + 0] = geometry2.attributes.position.array[i + 0] || 0
targetArr[i + 1] = geometry2.attributes.position.array[i + 1] || 0
targetArr[i + 2] = geometry2.attributes.position.array[i + 2] || 0
let rand = Math.random()
morphFactorArr[i + 0] = rand
morphFactorArr[i + 1] = rand
morphFactorArr[i + 2] = rand
}
this.addAttribute('a_target', new THREE.BufferAttribute(targetArr, 3))
this.addAttribute('a_morphFactor', new THREE.BufferAttribute(morphFactorArr, 1))
this.addAttribute('position', geometry1.attributes.position)
}
}
Then in my shaders I can simply transition between them like this:
vec3 new_position = mix(position, a_targetPosition, a_morphFactor);
This works, but is really dull and boring. The vertices simply map from one model to another, without any offsets, no gravity or whatever you want to throw into the mix.
Also, since I am attaching 0 to a position if there is a vert number mismatch, the unused positions simply scale to vec4(0.0, 0.0, 0.0, 1.0), which results in, again, a dull and boring effect (here morphing between a bunny and elephant models)
(notice how the unused bunny vertices simply scale down to 0)
How does one approach a problem like this?
Also, in the League of Legends link, how do they manage to
Animate the vertices internally to the model while it is active on the screen
Apply different velocity and gravity to the particles when mapping them to the next model (when clicking on the arrows and transitioning)?
Is it by passing a boolean attribute? Are they changing the targetPositions array? Any help is more than appreciated
This works, but is really dull and boring. The vertices simply map from one model to another, without any offsets, no gravity or whatever you want to throw into the mix.
So you can apply any effects you can imagine and code.
This is not the exact answer to your question, but this is the simplest motivating example of what you can do with shaders. Spoiler: the link to a working example is at the end of this answer.
Let's transform this
into this
with funny swarming particles during transition
We'll use THREE.BoxBufferGeometry()
with some custom attributes:
var sideLenght = 10;
var sideDivision = 50;
var cubeGeom = new THREE.BoxBufferGeometry(sideLenght, sideLenght, sideLenght, sideDivision, sideDivision, sideDivision);
var attrPhi = new Float32Array( cubeGeom.attributes.position.count );
var attrTheta = new Float32Array( cubeGeom.attributes.position.count );
var attrSpeed = new Float32Array( cubeGeom.attributes.position.count );
var attrAmplitude = new Float32Array( cubeGeom.attributes.position.count );
var attrFrequency = new Float32Array( cubeGeom.attributes.position.count );
for (var attr = 0; attr < cubeGeom.attributes.position.count; attr++){
attrPhi[attr] = Math.random() * Math.PI * 2;
attrTheta[attr] = Math.random() * Math.PI * 2;
attrSpeed[attr] = THREE.Math.randFloatSpread(6);
attrAmplitude[attr] = Math.random() * 5;
attrFrequency[attr] = Math.random() * 5;
}
cubeGeom.addAttribute( 'phi', new THREE.BufferAttribute( attrPhi, 1 ) );
cubeGeom.addAttribute( 'theta', new THREE.BufferAttribute( attrTheta, 1 ) );
cubeGeom.addAttribute( 'speed', new THREE.BufferAttribute( attrSpeed, 1 ) );
cubeGeom.addAttribute( 'amplitude', new THREE.BufferAttribute( attrAmplitude, 1 ) );
cubeGeom.addAttribute( 'frequency', new THREE.BufferAttribute( attrFrequency, 1 ) );
and THREE.ShaderMaterial()
:
var vertexShader = [
"uniform float interpolation;",
"uniform float radius;",
"uniform float time;",
"attribute float phi;",
"attribute float theta;",
"attribute float speed;",
"attribute float amplitude;",
"attribute float frequency;",
"vec3 rtp2xyz(){ // the magic is here",
" float tmpTheta = theta + time * speed;",
" float tmpPhi = phi + time * speed;",
" float r = sin(time * frequency) * amplitude * sin(interpolation * 3.1415926);",
" float x = sin(tmpTheta) * cos(tmpPhi) * r;",
" float y = sin(tmpTheta) * sin(tmpPhi) * r;",
" float z = cos(tmpPhi) * r;",
" return vec3(x, y, z);",
"}",
"void main(){",
" vec3 newPosition = mix(position, normalize(position) * radius, interpolation);",
" newPosition += rtp2xyz();",
" vec4 mvPosition = modelViewMatrix * vec4( newPosition, 1.0 );",
" gl_PointSize = 1. * ( 1. / length( mvPosition.xyz ) );",
" gl_Position = projectionMatrix * mvPosition;",
"}"
].join("\n");
var fragmentShader = [
"uniform vec3 color;",
"void main(){",
" gl_FragColor = vec4( color, 1.0 );",
"}"
].join("\n");
var uniforms = {
interpolation: { value: slider.value},
radius: { value: 7.5},
color: { value: new THREE.Color(0x00ff00)},
time: { value: 0 }
}
var shaderMat = new THREE.ShaderMaterial({
uniforms: uniforms,
vertexShader: vertexShader,
fragmentShader: fragmentShader,
//wireframe: true //just in case, if you want to use THREE.Mesh() instead of THREE.Points()
});
As you can see, all the magic happens in the vertex shader and its rtp2xyz()
function.
And in the end, the code of the function of animation:
var clock = new THREE.Clock();
var timeVal = 0;
render();
function render(){
timeVal += clock.getDelta();
requestAnimationFrame(render);
uniforms.time.value = timeVal;
uniforms.interpolation.value = slider.value;
renderer.render(scene, camera);
}
Oh, and yes, we have a slider control in our page:
<input id="slider" type="range" min="0" max="1" step="0.01" value="0.5" style="position:absolute;width:300px;">
Here's a snippet
var scene = new THREE.Scene();
var camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 1, 1000);
camera.position.set(10, 10, 20);
var renderer = new THREE.WebGLRenderer({antialias: true});
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
var controls = new THREE.OrbitControls(camera, renderer.domElement);
var vertexShader = [
"uniform float interpolation;",
"uniform float radius;",
"uniform float time;",
"attribute float phi;",
"attribute float theta;",
"attribute float speed;",
"attribute float amplitude;",
"attribute float frequency;",
"vec3 rtp2xyz(){ // the magic is here",
" float tmpTheta = theta + time * speed;",
" float tmpPhi = phi + time * speed;",
" float r = sin(time * frequency) * amplitude * sin(interpolation * 3.1415926);",
" float x = sin(tmpTheta) * cos(tmpPhi) * r;",
" float y = sin(tmpTheta) * sin(tmpPhi) * r;",
" float z = cos(tmpPhi) * r;",
" return vec3(x, y, z);",
"}",
"void main(){",
" vec3 newPosition = mix(position, normalize(position) * radius, interpolation);",
" newPosition += rtp2xyz();",
" vec4 mvPosition = modelViewMatrix * vec4( newPosition, 1.0 );",
" gl_PointSize = 1. * ( 1. / length( mvPosition.xyz ) );",
" gl_Position = projectionMatrix * mvPosition;",
"}"
].join("\n");
var fragmentShader = [
"uniform vec3 color;",
"void main(){",
" gl_FragColor = vec4( color, 1.0 );",
"}"
].join("\n");
var uniforms = {
interpolation: { value: slider.value},
radius: { value: 7.5},
color: { value: new THREE.Color(0x00ff00)},
time: { value: 0 }
}
var sideLenght = 10;
var sideDivision = 50;
var cubeGeom = new THREE.BoxBufferGeometry(sideLenght, sideLenght, sideLenght, sideDivision, sideDivision, sideDivision);
var attrPhi = new Float32Array( cubeGeom.attributes.position.count );
var attrTheta = new Float32Array( cubeGeom.attributes.position.count );
var attrSpeed = new Float32Array( cubeGeom.attributes.position.count );
var attrAmplitude = new Float32Array( cubeGeom.attributes.position.count );
var attrFrequency = new Float32Array( cubeGeom.attributes.position.count );
for (var attr = 0; attr < cubeGeom.attributes.position.count; attr++){
attrPhi[attr] = Math.random() * Math.PI * 2;
attrTheta[attr] = Math.random() * Math.PI * 2;
attrSpeed[attr] = THREE.Math.randFloatSpread(6);
attrAmplitude[attr] = Math.random() * 5;
attrFrequency[attr] = Math.random() * 5;
}
cubeGeom.addAttribute( 'phi', new THREE.BufferAttribute( attrPhi, 1 ) );
cubeGeom.addAttribute( 'theta', new THREE.BufferAttribute( attrTheta, 1 ) );
cubeGeom.addAttribute( 'speed', new THREE.BufferAttribute( attrSpeed, 1 ) );
cubeGeom.addAttribute( 'amplitude', new THREE.BufferAttribute( attrAmplitude, 1 ) );
cubeGeom.addAttribute( 'frequency', new THREE.BufferAttribute( attrFrequency, 1 ) );
var shaderMat = new THREE.ShaderMaterial({
uniforms: uniforms,
vertexShader: vertexShader,
fragmentShader: fragmentShader,
//wireframe: true
});
var points = new THREE.Points(cubeGeom, shaderMat);
scene.add(points);
var clock = new THREE.Clock();
var timeVal = 0;
render();
function render(){
timeVal += clock.getDelta();
requestAnimationFrame(render);
uniforms.time.value = timeVal;
uniforms.interpolation.value = slider.value;
renderer.render(scene, camera);
}
body{
margin: 0;
}
<script src="https://threejs.org/build/three.min.js"></script>
<script src="https://threejs.org/examples/js/controls/OrbitControls.js"></script>
<input id="slider" type="range" min="0" max="1" step="0.01" value="0.5" style="position:absolute;width:300px;">