Floating point texture test...
A Pen by Kilian K Lindberg on CodePen.
Floating point texture test...
A Pen by Kilian K Lindberg on CodePen.
| <canvas id="canvas"></canvas> |
| function App() { | |
| const conf = { | |
| el: 'canvas', | |
| fov: 75, | |
| cameraZ: 100, | |
| background: 0x000000, | |
| }; | |
| let renderer, scene, camera, cameraCtrl; | |
| let width, height, cx, cy, wWidth, wHeight; | |
| const { randFloat: rnd, randFloatSpread: rndFS } = THREE.Math; | |
| let ripple; | |
| let gridWWidth, gridWHeight, 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() { | |
| renderer = new THREE.WebGLRenderer({ canvas: document.getElementById(conf.el), antialias: true }); | |
| // if (!renderer.extensions.get('OES_texture_float')) { | |
| // alert('no floating point texture support'); | |
| // return; | |
| // } | |
| camera = new THREE.PerspectiveCamera(conf.fov); | |
| camera.position.z = conf.cameraZ; | |
| updateSize(); | |
| window.addEventListener('resize', updateSize, false); | |
| gridWWidth = wWidth; | |
| gridWHeight = wHeight; | |
| gridWidth = gridWWidth * width / wWidth; | |
| gridHeight = gridWHeight * height / wHeight; | |
| ripple = new RippleEffect(renderer, gridWidth, gridHeight, 1 / 5); | |
| 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('mouseleave', e => { mouseOver = false; }); | |
| renderer.domElement.addEventListener('mousemove', e => { | |
| mouseOver = true; | |
| const gp = getGridMP(e); | |
| ripple.addDrop(gp.x, gp.y, 0.05, 0.1); | |
| }); | |
| renderer.domElement.addEventListener('mouseup', e => { | |
| const gp = getGridMP(e); | |
| ripple.addDrop(gp.x, gp.y, 0.1, -1.5); | |
| }); | |
| initScene(); | |
| animate(); | |
| } | |
| function initScene() { | |
| scene = new THREE.Scene(); | |
| if (conf.background) scene.background = new THREE.Color(conf.background); | |
| const material = new THREE.MeshBasicMaterial({ | |
| color: 0xFFFFFF, side: THREE.DoubleSide, 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); | |
| transformed.z = 20. * info.r; | |
| `; | |
| shader.vertexShader = shader.vertexShader.replace(token, customTransform); | |
| } | |
| }); | |
| const pSize = 1; | |
| let nx = Math.round(gridWidth / 5), ny = Math.round(gridHeight / 50); | |
| let dy = gridWHeight / ny; | |
| for (let j = 0; j <= ny; j++) { | |
| const geometry = new THREE.PlaneBufferGeometry(gridWWidth, pSize, nx, 1); | |
| geometry.translate(0, - gridWHeight / 2 + j * dy, 0); | |
| const uvH = pSize / gridWHeight; | |
| const uvY = j / ny; | |
| const uvs = geometry.attributes.uv.array; | |
| for (let i = 0; i < uvs.length; i += 2) { | |
| uvs[i + 1] = (uvs[i + 1] == 0) ? uvY - uvH : uvY + uvH; | |
| } | |
| scene.add(new THREE.Mesh(geometry, material)); | |
| } | |
| nx = Math.round(gridWidth / 50); ny = Math.round(gridHeight / 5); | |
| let dx = gridWWidth / nx; | |
| for (let i = 0; i <= nx; i++) { | |
| const geometry = new THREE.PlaneBufferGeometry(pSize, gridWHeight, 1, ny); | |
| geometry.translate(- gridWWidth / 2 + i * dx, 0, 0); | |
| const uvW = pSize / gridWWidth; | |
| const uvX = i / nx; | |
| const uvs = geometry.attributes.uv.array; | |
| for (let i = 0; i < uvs.length; i += 2) { | |
| uvs[i] = (uvs[i] == 0) ? uvX - uvW : uvX + uvW; | |
| } | |
| scene.add(new THREE.Mesh(geometry, material)); | |
| } | |
| camera.position.set(0, -gridWHeight/1.6, 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.4; | |
| const y = Math.sin(time) * 0.4; | |
| ripple.addDrop(x, y, 0.05, -0.05); | |
| } | |
| 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, ratio) { | |
| this.renderer = renderer; | |
| this.width = Math.round(ratio * width); | |
| this.height = Math.round(ratio * height); | |
| this.delta = new THREE.Vector2(ratio / width, ratio / 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(); | |
| } | |
| // From https://github.com/evanw/webgl-water | |
| 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.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(); | |
| }; | |
| RippleEffect.prototype.updateHMap = function () { | |
| this.updateMat.uniforms.tDiffuse.value = this.hMap.texture; | |
| this.renderShaderMat(this.updateMat, 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(); |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/110/three.js"></script> | |
| <script src="https://klevron.github.io/codepen/three.js/OrbitControls.110.js"></script> |
| html, body { | |
| margin: 0; | |
| } | |
| canvas { | |
| display: block; | |
| } |