Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save rolandkorgowski/778a42db9b15548f3ea11c27cc6165e8 to your computer and use it in GitHub Desktop.
Save rolandkorgowski/778a42db9b15548f3ea11c27cc6165e8 to your computer and use it in GitHub Desktop.
3D Image Transition (mouse wheel)

3D Image Transition (mouse wheel)

Simple image gallery with 3D transition using InstancedMesh and shaders.

Use mouse wheel or arrow keys :)

A Pen by Kevin Levron on CodePen.

License.

<canvas id="canvas"></canvas>
<p class="collection">
<a href="https://codepen.io/collection/AGZywR" target="_blank">WebGL Collection</a>
</p>
import {
BufferGeometry,
Color,
DoubleSide,
Face3,
Geometry,
InstancedBufferAttribute,
InstancedMesh,
MathUtils,
MeshBasicMaterial,
Object3D,
PerspectiveCamera,
Scene,
TextureLoader,
Vector2,
Vector3,
WebGLRenderer
} from 'https://unpkg.com/three@0.119.0/build/three.module.js';
function App() {
const conf = {
size: 10,
images: [
{ src: 'https://assets.codepen.io/33787/img1.jpg' },
{ src: 'https://assets.codepen.io/33787/img2.jpg' },
{ src: 'https://assets.codepen.io/33787/img3.jpg' },
{ src: 'https://assets.codepen.io/33787/img4.jpg' }
]
};
let renderer, scene, camera, cameraCtrl;
const screen = {
width: 0, height: 0,
wWidth: 0, wHeight: 0,
ratio: 0
};
const loader = new TextureLoader();
const textures = [];
let planes, plane1, plane2;
let progress = 0, targetProgress = 0;
const mouse = new Vector2();
init();
function init() {
renderer = new WebGLRenderer({ canvas: document.getElementById('canvas'), antialias: true });
camera = new PerspectiveCamera(50);
camera.position.z = 150;
updateSize();
window.addEventListener('resize', onResize);
Promise.all(conf.images.map(loadTexture)).then(responses => {
initScene();
initListeners();
gsap.fromTo(plane1.uProgress,
{
value: -2
},
{
value: 0,
duration: 2.5,
ease: Power4.easeOut
}
);
requestAnimationFrame(animate);
});
}
function initScene() {
scene = new Scene();
scene.background = new Color(0);
plane1 = new AnimatedPlane({
renderer, screen,
size: conf.size,
anim: 1,
texture: textures[0]
});
plane2 = new AnimatedPlane({
renderer, screen,
size: conf.size,
anim: 2,
texture: textures[1]
});
setPlanesProgress(0);
planes = new Object3D();
planes.add(plane1.o3d);
planes.add(plane2.o3d);
scene.add(planes);
}
function initListeners() {
document.addEventListener('mousemove', e => {
mouse.x = (e.clientX / screen.width) * 2 - 1;
mouse.y = -(e.clientY / screen.height) * 2 + 1;
});
window.addEventListener('wheel', e => {
e.preventDefault();
if (e.deltaY > 0) {
targetProgress = limit(targetProgress + 1 / 20, 0, conf.images.length - 1);
} else {
targetProgress = limit(targetProgress - 1 / 20, 0, conf.images.length - 1);
}
});
document.addEventListener('click', e => {
if (e.clientY < screen.height / 2) {
navPrevious();
} else {
navNext();
}
});
document.addEventListener('keyup', e => {
if (e.keyCode === 37 || e.keyCode === 38) {
navPrevious();
} else if (e.keyCode === 39 || e.keyCode === 40) {
navNext();
}
});
}
function navNext() {
if (Number.isInteger(targetProgress)) targetProgress += 1;
else targetProgress = Math.ceil(targetProgress);
targetProgress = limit(targetProgress, 0, conf.images.length - 1);
}
function navPrevious() {
if (Number.isInteger(targetProgress)) targetProgress -= 1;
else targetProgress = Math.floor(targetProgress);
targetProgress = limit(targetProgress, 0, conf.images.length - 1);
}
function updateProgress() {
const progress1 = lerp(progress, targetProgress, 0.1);
const pdiff = progress1 - progress;
if (pdiff === 0) return;
const p0 = progress % 1;
const p1 = progress1 % 1;
if ((pdiff > 0 && p1 < p0) || (pdiff < 0 && p0 < p1)) {
const i = Math.floor(progress1);
plane1.setTexture(textures[i]);
plane2.setTexture(textures[i + 1]);
}
progress = progress1;
setPlanesProgress(progress % 1);
}
function setPlanesProgress(progress) {
plane1.uProgress.value = progress;
plane2.uProgress.value = -1 + progress;
plane1.material.opacity = 1 - progress;
plane2.material.opacity = progress;
plane1.o3d.position.z = progress;
plane2.o3d.position.z = progress - 1;
}
function animate() {
requestAnimationFrame(animate);
updateProgress();
const tiltX = lerp(planes.rotation.x, -mouse.y * 0.2, 0.1);
const tiltY = lerp(planes.rotation.y, mouse.x * 0.2, 0.1);
planes.rotation.set(tiltX, tiltY, 0);
renderer.render(scene, camera);
}
let resizeTimeout;
function onResize() {
clearTimeout(resizeTimeout);
resizeTimeout = setTimeout(updateSize, 200);
}
function updateSize() {
screen.width = window.innerWidth;
screen.height = window.innerHeight;
screen.ratio = screen.width / screen.height;
if (renderer && camera) {
renderer.setSize(screen.width, screen.height);
camera.aspect = screen.ratio;
camera.updateProjectionMatrix();
const wsize = getRendererSize();
screen.wWidth = wsize[0]; screen.wHeight = wsize[1];
}
if (plane1) plane1.resize();
if (plane2) plane2.resize();
}
function getRendererSize() {
const vFOV = (camera.fov * Math.PI) / 180;
const h = 2 * Math.tan(vFOV / 2) * Math.abs(camera.position.z);
const w = h * camera.aspect;
return [w, h];
}
function loadTexture(img, index) {
return new Promise(resolve => {
loader.load(
img.src,
texture => {
textures[index] = texture;
resolve(texture);
}
);
});
}
}
class AnimatedPlane {
constructor(params) {
for (const [key, value] of Object.entries(params)) {
this[key] = value;
}
this.o3d = new Object3D();
this.uProgress = { value: 0 };
this.uvScale = new Vector2();
this.initMaterial();
this.initPlane();
}
initMaterial() {
this.material = new MeshBasicMaterial({
side: DoubleSide,
transparent: true,
map: this.texture,
onBeforeCompile: shader => {
shader.uniforms.progress = this.uProgress;
shader.uniforms.uvScale = { value: this.uvScale };
shader.vertexShader = `
uniform float progress;
uniform vec2 uvScale;
attribute vec3 offset;
attribute vec3 rotation;
attribute vec2 uvOffset;
mat3 rotationMatrixXYZ(vec3 r)
{
float cx = cos(r.x);
float sx = sin(r.x);
float cy = cos(r.y);
float sy = sin(r.y);
float cz = cos(r.z);
float sz = sin(r.z);
return mat3(
cy * cz, cx * sz + sx * sy * cz, sx * sz - cx * sy * cz,
-cy * sz, cx * cz - sx * sy * sz, sx * cz + cx * sy * sz,
sy, -sx * cy, cx * cy
);
}
` + shader.vertexShader;
shader.vertexShader = shader.vertexShader.replace('#include <uv_vertex>', `
#include <uv_vertex>
vUv = vUv * uvScale + uvOffset;
`);
shader.vertexShader = shader.vertexShader.replace('#include <project_vertex>', `
mat3 rotMat = rotationMatrixXYZ(progress * rotation);
transformed = rotMat * transformed;
vec4 mvPosition = vec4(transformed, 1.0);
#ifdef USE_INSTANCING
mvPosition = instanceMatrix * mvPosition;
#endif
mvPosition.xyz += progress * offset;
mvPosition = modelViewMatrix * mvPosition;
gl_Position = projectionMatrix * mvPosition;
`);
}
});
}
initPlane() {
const { width, wWidth, wHeight } = this.screen;
this.wSize = this.size * wWidth / width;
this.nx = Math.ceil(wWidth / this.wSize) + 1;
this.ny = Math.ceil(wHeight / this.wSize) + 1;
this.icount = this.nx * this.ny;
this.initGeometry();
this.initUV();
this.initAnimAttributes();
if (this.imesh) {
this.o3d.remove(this.imesh);
}
this.imesh = new InstancedMesh(this.bGeometry, this.material, this.icount);
this.o3d.add(this.imesh);
const dummy = new Object3D();
let index = 0;
let x = -(wWidth - (wWidth - this.nx * this.wSize)) / 2 + this.dx;
for (let i = 0; i < this.nx; i++) {
let y = -(wHeight - (wHeight - this.ny * this.wSize)) / 2 + this.dy;
for (let j = 0; j < this.ny; j++) {
dummy.position.set(x, y, 0);
dummy.updateMatrix();
this.imesh.setMatrixAt(index++, dummy.matrix);
y += this.wSize;
}
x += this.wSize;
}
}
initGeometry() {
// square
const geometry = new Geometry();
geometry.vertices.push(new Vector3(0, 0, 0));
geometry.vertices.push(new Vector3(this.wSize, 0, 0));
geometry.vertices.push(new Vector3(0, this.wSize, 0));
geometry.vertices.push(new Vector3(this.wSize, this.wSize, 0));
geometry.faces.push(new Face3(0, 2, 1));
geometry.faces.push(new Face3(2, 3, 1));
geometry.faceVertexUvs[0].push([
new Vector2(0, 0),
new Vector2(0, 1),
new Vector2(1, 0)
]);
geometry.faceVertexUvs[0].push([
new Vector2(0, 1),
new Vector2(1, 1),
new Vector2(1, 0)
]);
// geometry.computeFaceNormals();
// geometry.computeVertexNormals();
// center
this.dx = this.wSize / 2;
this.dy = this.wSize / 2;
geometry.translate(-this.dx, -this.dy, 0);
this.bGeometry = new BufferGeometry().fromGeometry(geometry);
}
initAnimAttributes() {
const { randFloat: rnd, randFloatSpread: rndFS } = MathUtils;
const v3 = new Vector3();
const offsets = new Float32Array(this.icount * 3);
for (let i = 0; i < offsets.length; i += 3) {
if (this.anim === 1) v3.set(rndFS(10), rnd(50, 100), rnd(20, 50)).toArray(offsets, i);
else v3.set(rndFS(20), rndFS(20), rnd(20, 200)).toArray(offsets, i);
}
this.bGeometry.setAttribute('offset', new InstancedBufferAttribute(offsets, 3));
const rotations = new Float32Array(this.icount * 3);
const angle = Math.PI * 4;
for (let i = 0; i < rotations.length; i += 3) {
rotations[i] = rndFS(angle);
rotations[i + 1] = rndFS(angle);
rotations[i + 2] = rndFS(angle);
}
this.bGeometry.setAttribute('rotation', new InstancedBufferAttribute(rotations, 3));
}
initUV() {
const ratio = this.nx / this.ny;
const tRatio = this.texture.image.width / this.texture.image.height;
if (ratio > tRatio) this.uvScale.set(1 / this.nx, (tRatio / ratio) / this.ny);
else this.uvScale.set((ratio / tRatio) / this.nx, 1 / this.ny);
const nW = this.uvScale.x * this.nx;
const nH = this.uvScale.y * this.ny;
const v2 = new Vector2();
const uvOffsets = new Float32Array(this.icount * 2);
for (let i = 0; i < this.nx; i++) {
for (let j = 0; j < this.ny; j++) {
v2.set(
this.uvScale.x * i + (1 - nW) / 2,
this.uvScale.y * j + (1 - nH) / 2
).toArray(uvOffsets, (i * this.ny + j) * 2);
}
}
this.bGeometry.setAttribute('uvOffset', new InstancedBufferAttribute(uvOffsets, 2));
}
setTexture(texture) {
this.texture = texture;
this.material.map = texture;
this.initUV();
}
resize() {
this.initPlane();
}
}
function limit(val, min, max) {
return val < min ? min : (val > max ? max : val);
}
function lerp(a, b, x) {
return a + x * (b - a);
}
App();
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.4.2/gsap.min.js"></script>
body {
margin: 0;
width: 100%;
height: 100%;
}
canvas {
display: block;
cursor: pointer;
}
<link href="https://codepen.io/soju22/pen/4aeaa568d7a4f3459bcde0e70b10f35a" rel="stylesheet" />
<link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@900&amp;display=swap" rel="stylesheet" />
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment