diff --git a/src/components/liquid/LiquidEffect.js b/src/components/liquid/LiquidEffect.js new file mode 100644 index 0000000..10d08df --- /dev/null +++ b/src/components/liquid/LiquidEffect.js @@ -0,0 +1,194 @@ +import { + FloatType, + Mesh, + OrthographicCamera, + PlaneBufferGeometry, + RGBAFormat, + ShaderMaterial, + Uniform, + Vector2, + WebGLRenderTarget, +} from 'three'; + +function LiquidEffect(renderer) { + this.renderer = renderer; + this.width = 512; + this.height = 512; + // this.delta = new Vector2(this.width / Math.pow(width, 2), this.height / Math.pow(height, 2)); + this.delta = new Vector2(1 / this.width, 1 / this.height); + + this.hMap = new WebGLRenderTarget(this.width, this.height, { type: FloatType, format: RGBAFormat, depthBuffer: false, stencilBuffer: false }); + this.hMap1 = new WebGLRenderTarget(this.width, this.height, { type: FloatType, format: RGBAFormat, depthBuffer: false, stencilBuffer: false }); + this.fsQuad = new FullScreenQuad(); + + this.initShaders(); +}; + +LiquidEffect.prototype.initShaders = function () { + const defaultVertexShader = ` + varying vec2 vUv; + void main() { + vUv = uv; + gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); + } + `; + + this.copyMat = new ShaderMaterial({ + uniforms: { tDiffuse: { value: null } }, + vertexShader: defaultVertexShader, + fragmentShader: ` + uniform sampler2D tDiffuse; + varying vec2 vUv; + void main() { + gl_FragColor = texture2D(tDiffuse, vUv); + } + `, + }); + + this.updateMat = new ShaderMaterial({ + uniforms: { + tDiffuse: { value: null }, + delta: new 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 ShaderMaterial({ + uniforms: { + tDiffuse: { value: null }, + delta: new 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 ShaderMaterial({ + uniforms: { + tDiffuse: { value: null }, + center: new Uniform(new 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; + } + `, + }); +}; + +LiquidEffect.prototype.update = function () { + this.updateHMap(); + // this.updateHMap(); + this.updateHMapNormals(); +}; + +LiquidEffect.prototype.updateHMap = function () { + this.updateMat.uniforms.tDiffuse.value = this.hMap.texture; + this.renderShaderMat(this.updateMat, this.hMap1); + this.swapBuffers(); +}; + +LiquidEffect.prototype.updateHMapNormals = function () { + this.normalsMat.uniforms.tDiffuse.value = this.hMap.texture; + this.renderShaderMat(this.normalsMat, this.hMap1); + this.swapBuffers(); +}; + +LiquidEffect.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(); +}; + +LiquidEffect.prototype.renderBuffer = function (buffer, target) { + this.copyMat.uniforms.tDiffuse.value = buffer.texture; + this.renderShaderMat(this.copyMat, target); +}; + +LiquidEffect.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); +}; + +LiquidEffect.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 OrthographicCamera(-1, 1, 1, -1, 0, 1); + const geometry = new PlaneBufferGeometry(2, 2); + + const FullScreenQuad = function (material) { + this._mesh = new 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; +})(); + +export default LiquidEffect; diff --git a/src/components/liquid/LiquidPlane.js b/src/components/liquid/LiquidPlane.js new file mode 100644 index 0000000..881d8b1 --- /dev/null +++ b/src/components/liquid/LiquidPlane.js @@ -0,0 +1,54 @@ +import { DoubleSide, Mesh, MeshStandardMaterial, PlaneBufferGeometry } from 'three'; +import { watch } from 'vue'; +import Object3D from '../../core/Object3D.js'; +import { bindProps } from '../../tools.js'; +import LiquidEffect from './LiquidEffect.js'; + +export default { + extends: Object3D, + props: { + width: { type: Number, default: 10 }, + height: { type: Number, default: 10 }, + widthSegments: { type: Number, default: 200 }, + heightSegments: { type: Number, default: 200 }, + color: { type: [Number, String], default: '#ffffff' }, + metalness: { type: Number, default: 0.75 }, + roughness: { type: Number, default: 0.25 }, + }, + mounted() { + this.liquidEffect = new LiquidEffect(this.three.renderer); + this.rendererComponent.onMounted(() => { + this.liquidEffect.renderer = this.rendererComponent.renderer; + this.three.onBeforeRender(() => { + this.liquidEffect.update(); + }); + }); + + this.material = new MeshStandardMaterial({ color: this.color, side: DoubleSide, metalness: this.metalness, roughness: this.roughness, + onBeforeCompile: shader => { + shader.uniforms.hmap = { value: this.liquidEffect.hMap.texture }; + shader.vertexShader = "uniform sampler2D hmap;\n" + shader.vertexShader; + const token = '#include '; + 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); + }, + }); + bindProps(this, ['metalness', 'roughness'], this.material); + watch(() => this.color, (value) => this.material.color.set(value)); + + this.geometry = new PlaneBufferGeometry(this.width, this.height, this.widthSegments, this.heightSegments); + this.mesh = new Mesh(this.geometry, this.material); + this.initObject3D(this.mesh); + }, + // mounted() { + // }, + // unmounted() { + // }, + methods: { + }, +};