<canvas id="canvas"></canvas>
html, body {
margin: 0;
}
canvas {
display: block;
}
function App() {
const conf = {
el: 'canvas',
fov: 75,
cameraZ: 100,
};
let renderer, scene, camera, cameraCtrl;
let width, height, cx, cy, wWidth, wHeight;
let ripple;
let gridWWidth, gridWHeight;
let gridWidth, gridHeight;
const mouse = new THREE.Vector2();
const mousePlane = new THREE.Plane(new THREE.Vector3(0, 0, 1), 0);
const mousePosition = new THREE.Vector3();
const raycaster = new THREE.Raycaster();
let mouseOver = false;
init();
function init() {
// const gl = renderer.getContext();
// const floatTextures = gl.getExtension('OES_texture_float');
// if (!floatTextures) {
// alert('no floating point texture support');
// return;
// }
renderer = new THREE.WebGLRenderer({ canvas: document.getElementById(conf.el), antialias: true });
camera = new THREE.PerspectiveCamera(conf.fov);
camera.position.z = conf.cameraZ;
updateSize();
window.addEventListener('resize', updateSize, false);
// gridWHeight = wHeight - 20;
// gridWWidth = gridWHeight;
gridWHeight = wHeight;
gridWWidth = wWidth;
gridWidth = gridWWidth * width / wWidth;
gridHeight = gridWHeight * height / wHeight;
ripple = new RippleEffect(renderer, width, height);
const getGridMP = function (e) {
const v = new THREE.Vector3();
camera.getWorldDirection(v);
v.normalize();
mouse.x = ((e.clientX / width) * 2 - 1);
mouse.y = (-(e.clientY / height) * 2 + 1);
raycaster.setFromCamera(mouse, camera);
raycaster.ray.intersectPlane(mousePlane, mousePosition);
return { x: 2 * mousePosition.x / gridWWidth, y: 2 * mousePosition.y / gridWHeight };
};
renderer.domElement.addEventListener('mousemove', e => {
mouseOver = true;
const gp = getGridMP(e);
ripple.addDrop(gp.x, gp.y, 0.05, 0.1);
});
renderer.domElement.addEventListener('mouseleave', e => { mouseOver = false; });
// renderer.domElement.addEventListener('mouseup', e => {
// const gp = getGridMP(e);
// ripple.addDrop(gp.x, gp.y, 0.2, -3.0);
// });
initScene();
animate();
}
function initScene() {
scene = new THREE.Scene();
let pointLight1 = new THREE.PointLight(0xFFFF80);
pointLight1.position.set(-wWidth / 2, wHeight / 2, 50);
scene.add(pointLight1);
let pointLight2 = new THREE.PointLight(0xde3578);
pointLight2.position.set(wWidth / 2, wHeight / 2, 50);
scene.add(pointLight2);
let pointLight3 = new THREE.PointLight(0xFF4040);
pointLight3.position.set(-wWidth / 2, -wHeight / 2, 50);
scene.add(pointLight3);
let pointLight4 = new THREE.PointLight(0x0247e5);
pointLight4.position.set(wWidth / 2, -wHeight / 2, 50);
scene.add(pointLight4);
renderer.domElement.addEventListener('mouseup', e => {
pointLight1.color = new THREE.Color(chroma.random().hex());
pointLight2.color = new THREE.Color(chroma.random().hex());
pointLight3.color = new THREE.Color(chroma.random().hex());
pointLight4.color = new THREE.Color(chroma.random().hex());
});
const material = new THREE.MeshStandardMaterial({ color: 0xffffff, side: THREE.DoubleSide, metalness: 0.5, roughness: 0.5, onBeforeCompile: shader => {
shader.uniforms.hmap = { value: ripple.hMap.texture };
shader.vertexShader = "uniform sampler2D hmap;\n" + shader.vertexShader;
const token = '#include <begin_vertex>';
const customTransform = `
vec3 transformed = vec3(position);
vec4 info = texture2D(hmap, uv);
vNormal = vec3(info.b, sqrt(1.0 - dot(info.ba, info.ba)), info.a).xzy;
transformed.z = 20. * info.r;
`;
shader.vertexShader = shader.vertexShader.replace(token, customTransform);
} });
let nx = Math.round(gridWidth / 5), ny = Math.round(gridHeight / 40);
let dx = gridWWidth / nx, dy = gridWHeight / ny;
for (let j = 0; j <= ny; j++) {
const geometry = new THREE.BufferGeometry();
const positions = [], uvs = [];
const y = - gridWHeight / 2 + j * dy;
for (let i = 0; i <= nx; i++) {
positions.push(- gridWWidth / 2 + i * dx, y, 0);
uvs.push(i / nx, j / ny);
}
geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
geometry.setAttribute('uv', new THREE.Float32BufferAttribute(uvs, 2));
geometry.computeBoundingSphere();
scene.add(new THREE.Line(geometry, material));
}
nx = Math.round(gridWidth / 40); ny = Math.round(gridHeight / 5);
dx = gridWWidth / nx; dy = gridWHeight / ny;
for (let i = 0; i <= nx; i++) {
const geometry = new THREE.BufferGeometry();
const positions = [], uvs = [];
const x = - gridWWidth / 2 + i * dx;
for (let j = 0; j <= ny; j++) {
positions.push(x, - gridWHeight / 2 + j * dy, 0);
uvs.push(i / nx, j / ny);
}
geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
geometry.setAttribute('uv', new THREE.Float32BufferAttribute(uvs, 2));
geometry.computeBoundingSphere();
scene.add(new THREE.Line(geometry, material));
}
camera.position.set(0, -gridWHeight/2, 40);
camera.lookAt(new THREE.Vector3(0, -gridWHeight/6, 0));
cameraCtrl = new THREE.OrbitControls(camera, renderer.domElement);
cameraCtrl.enableDamping = true;
cameraCtrl.dampingFactor = 0.1;
cameraCtrl.rotateSpeed = 0.5;
}
function animate() {
if (!mouseOver) {
const time = Date.now() * 0.001;
const x = Math.cos(time) * 0.2;
const y = Math.sin(time) * 0.2;
ripple.addDrop(x, y, 0.05, -0.04);
}
ripple.update();
renderer.render(scene, camera);
requestAnimationFrame(animate);
}
function updateSize() {
width = window.innerWidth; cx = width / 2;
height = window.innerHeight; cy = height / 2;
renderer.setSize(width, height);
camera.aspect = width / height;
camera.updateProjectionMatrix();
const wsize = getRendererSize();
wWidth = wsize[0]; wHeight = wsize[1];
}
function getRendererSize() {
const cam = new THREE.PerspectiveCamera(camera.fov, camera.aspect);
const vFOV = (cam.fov * Math.PI) / 180;
const height = 2 * Math.tan(vFOV / 2) * Math.abs(conf.cameraZ);
const width = height * cam.aspect;
return [width, height];
}
}
const RippleEffect = (function () {
function RippleEffect(renderer, width, height) {
this.renderer = renderer;
this.width = 512;
this.height = 512;
// this.delta = new THREE.Vector2(this.width / Math.pow(width, 2), this.height / Math.pow(height, 2));
this.delta = new THREE.Vector2(1 / this.width, 1 / this.height);
this.hMap = new THREE.WebGLRenderTarget(this.width, this.height, { type: THREE.FloatType, depthBuffer: false, stencilBuffer: false });
this.hMap1 = new THREE.WebGLRenderTarget(this.width, this.height, { type: THREE.FloatType, depthBuffer: false, stencilBuffer: false });
this.fsQuad = new FullScreenQuad();
this.initShaders();
}
RippleEffect.prototype.initShaders = function () {
// default vertex shader
const defaultVertexShader = `
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`;
this.copyMat = new THREE.ShaderMaterial({
uniforms: { 'tDiffuse': { value: null } },
vertexShader: defaultVertexShader,
fragmentShader: `
uniform sampler2D tDiffuse;
varying vec2 vUv;
void main() {
gl_FragColor = texture2D(tDiffuse, vUv);
}
`,
});
this.updateMat = new THREE.ShaderMaterial({
uniforms: {
'tDiffuse': { value: null },
'delta': new THREE.Uniform(this.delta),
},
vertexShader: defaultVertexShader,
fragmentShader: `
uniform sampler2D tDiffuse;
uniform vec2 delta;
varying vec2 vUv;
void main() {
vec4 texel = texture2D(tDiffuse, vUv);
vec2 dx = vec2(delta.x, 0.0);
vec2 dy = vec2(0.0, delta.y);
float average = (
texture2D(tDiffuse, vUv - dx).r +
texture2D(tDiffuse, vUv - dy).r +
texture2D(tDiffuse, vUv + dx).r +
texture2D(tDiffuse, vUv + dy).r
) * 0.25;
texel.g += (average - texel.r) * 2.0;
texel.g *= 0.995;
texel.r += texel.g;
gl_FragColor = texel;
}
`,
});
this.normalsMat = new THREE.ShaderMaterial({
uniforms: {
'tDiffuse': { value: null },
'delta': new THREE.Uniform(this.delta),
},
vertexShader: defaultVertexShader,
fragmentShader: `
uniform sampler2D tDiffuse;
uniform vec2 delta;
varying vec2 vUv;
void main() {
vec4 texel = texture2D(tDiffuse, vUv);
vec3 dx = vec3(delta.x, texture2D(tDiffuse, vec2(vUv.x + delta.x, vUv.y)).r - texel.r, 0.0);
vec3 dy = vec3(0.0, texture2D(tDiffuse, vec2(vUv.x, vUv.y + delta.y)).r - texel.r, delta.y);
texel.ba = normalize(cross(dy, dx)).xz;
gl_FragColor = texel;
}
`,
});
this.dropMat = new THREE.ShaderMaterial({
uniforms: {
'tDiffuse': { value: null },
'center': new THREE.Uniform(new THREE.Vector2()),
'radius': { value: 0.05 },
'strength': { value: 0.5 },
},
vertexShader: defaultVertexShader,
fragmentShader: `
const float PI = 3.1415926535897932384626433832795;
uniform sampler2D tDiffuse;
uniform vec2 center;
uniform float radius;
uniform float strength;
varying vec2 vUv;
void main() {
vec4 texel = texture2D(tDiffuse, vUv);
float drop = max(0.0, 1.0 - length(center * 0.5 + 0.5 - vUv) / radius);
drop = 0.5 - cos(drop * PI) * 0.5;
texel.r += drop * strength;
// texel.r = clamp(texel.r, -2.0, 2.0);
gl_FragColor = texel;
}
`,
});
};
RippleEffect.prototype.update = function () {
this.updateHMap();
this.updateHMapNormals();
};
RippleEffect.prototype.updateHMap = function () {
this.updateMat.uniforms.tDiffuse.value = this.hMap.texture;
this.renderShaderMat(this.updateMat, this.hMap1);
this.swapBuffers();
};
RippleEffect.prototype.updateHMapNormals = function () {
this.normalsMat.uniforms.tDiffuse.value = this.hMap.texture;
this.renderShaderMat(this.normalsMat, this.hMap1);
this.swapBuffers();
};
RippleEffect.prototype.addDrop = function (x, y, radius, strength) {
this.dropMat.uniforms.tDiffuse.value = this.hMap.texture;
this.dropMat.uniforms.center.value.set(x, y);
this.dropMat.uniforms.radius.value = radius;
this.dropMat.uniforms.strength.value = strength;
this.renderShaderMat(this.dropMat, this.hMap1);
this.swapBuffers();
};
RippleEffect.prototype.renderBuffer = function (buffer, target) {
target = target ? target : null;
this.copyMat.uniforms.tDiffuse.value = buffer.texture;
this.renderShaderMat(this.copyMat, target);
};
RippleEffect.prototype.renderShaderMat = function (mat, target) {
this.fsQuad.material = mat;
const oldTarget = this.renderer.getRenderTarget();
this.renderer.setRenderTarget(target);
this.fsQuad.render(this.renderer);
this.renderer.setRenderTarget(oldTarget);
};
RippleEffect.prototype.swapBuffers = function () {
const temp = this.hMap;
this.hMap = this.hMap1;
this.hMap1 = temp;
};
// from https://threejs.org/examples/js/postprocessing/EffectComposer.js
const FullScreenQuad = (function () {
const camera = new THREE.OrthographicCamera(- 1, 1, 1, - 1, 0, 1);
const geometry = new THREE.PlaneBufferGeometry(2, 2);
const FullScreenQuad = function (material) {
this._mesh = new THREE.Mesh(geometry, material);
};
Object.defineProperty(FullScreenQuad.prototype, 'material', {
get: function () { return this._mesh.material; },
set: function (value) { this._mesh.material = value; }
});
Object.assign(FullScreenQuad.prototype, {
render: function (renderer) {
renderer.render(this._mesh, camera);
}
});
return FullScreenQuad;
})();
return RippleEffect;
})();
App();
This Pen doesn't use any external CSS resources.