From 12b5bb58913adf4a20daab786afef9ebd87174d2 Mon Sep 17 00:00:00 2001 From: alvarosabu Date: Tue, 12 Aug 2025 22:11:25 +0200 Subject: [PATCH 1/4] feat: add grainy liquid effect with Blob component and shaders - Introduced `Blob.vue` to create a dynamic grainy liquid effect using Three.js shaders. - Added `Experience.vue` to manage the scene setup, including camera and lighting configurations. - Created vertex and fragment shaders for realistic liquid movement and color mixing. - Implemented a global entry point in `index.global.vue` to render the grainy liquid experience. - Added TypeScript definitions for shader files to ensure type safety. - Documented the grainy liquid experiment in a new markdown file for clarity and reference. These additions enhance the visual effects and interactivity of the application, providing a more engaging user experience. --- app/components/grainy-liquid/Blob.vue | 115 ++++++++++++++++++ app/components/grainy-liquid/Experience.vue | 24 ++++ app/components/grainy-liquid/index.global.vue | 8 ++ .../grainy-liquid/shaders/fragment.glsl | 73 +++++++++++ .../grainy-liquid/shaders/fragment.glsl.d.ts | 2 + .../grainy-liquid/shaders/vertex.glsl | 109 +++++++++++++++++ .../grainy-liquid/shaders/vertex.glsl.d.ts | 2 + content/experiments/grainy-liquid.md | 7 ++ 8 files changed, 340 insertions(+) create mode 100644 app/components/grainy-liquid/Blob.vue create mode 100644 app/components/grainy-liquid/Experience.vue create mode 100644 app/components/grainy-liquid/index.global.vue create mode 100644 app/components/grainy-liquid/shaders/fragment.glsl create mode 100644 app/components/grainy-liquid/shaders/fragment.glsl.d.ts create mode 100644 app/components/grainy-liquid/shaders/vertex.glsl create mode 100644 app/components/grainy-liquid/shaders/vertex.glsl.d.ts create mode 100644 content/experiments/grainy-liquid.md diff --git a/app/components/grainy-liquid/Blob.vue b/app/components/grainy-liquid/Blob.vue new file mode 100644 index 00000000..b707464e --- /dev/null +++ b/app/components/grainy-liquid/Blob.vue @@ -0,0 +1,115 @@ + + + \ No newline at end of file diff --git a/app/components/grainy-liquid/Experience.vue b/app/components/grainy-liquid/Experience.vue new file mode 100644 index 00000000..5b76cd25 --- /dev/null +++ b/app/components/grainy-liquid/Experience.vue @@ -0,0 +1,24 @@ + + + \ No newline at end of file diff --git a/app/components/grainy-liquid/index.global.vue b/app/components/grainy-liquid/index.global.vue new file mode 100644 index 00000000..b64c1226 --- /dev/null +++ b/app/components/grainy-liquid/index.global.vue @@ -0,0 +1,8 @@ + + \ No newline at end of file diff --git a/app/components/grainy-liquid/shaders/fragment.glsl b/app/components/grainy-liquid/shaders/fragment.glsl new file mode 100644 index 00000000..82b7d06a --- /dev/null +++ b/app/components/grainy-liquid/shaders/fragment.glsl @@ -0,0 +1,73 @@ +uniform float u_time; +uniform vec3 u_colorA; +uniform vec3 u_colorB; +uniform vec3 u_colorC; +uniform float u_noiseScale; +uniform float u_grainIntensity; +uniform float u_fresnelPower; + +varying vec3 v_position; +varying vec3 v_normal; +varying vec2 v_uv; + +// Random function for grain effect +float random(vec2 st) { + return fract(sin(dot(st.xy, vec2(12.9898,78.233))) * 43758.5453123); +} + +// Noise function for color mixing +float noise(vec2 st) { + vec2 i = floor(st); + vec2 f = fract(st); + + float a = random(i); + float b = random(i + vec2(1.0, 0.0)); + float c = random(i + vec2(0.0, 1.0)); + float d = random(i + vec2(1.0, 1.0)); + + vec2 u = f * f * (3.0 - 2.0 * f); + + return mix(a, b, u.x) + (c - a)* u.y * (1.0 - u.x) + (d - b) * u.x * u.y; +} + +// Fractal Brownian Motion for more complex noise +float fbm(vec2 st) { + float value = 0.0; + float amplitude = 0.5; + float frequency = 1.0; + + for (int i = 0; i < 5; i++) { + value += amplitude * noise(st * frequency); + frequency *= 2.0; + amplitude *= 0.5; + } + + return value; +} + +void main() { + vec3 normal = normalize(v_normal); + vec3 viewDirection = normalize(cameraPosition - v_position); + + // Fresnel effect for liquid-like rim lighting + float fresnel = 1.0 - dot(normal, viewDirection); + fresnel = pow(fresnel, u_fresnelPower); + + // Create flowing noise pattern for color mixing + vec2 flowUv = v_uv + u_time * 0.1; + float colorNoise = fbm(flowUv * u_noiseScale); + + // Mix colors based on noise and fresnel + vec3 baseColor = mix(u_colorA, u_colorB, colorNoise); + vec3 finalColor = mix(baseColor, u_colorC, fresnel); + + // Add grain effect + float grain = random(v_uv + u_time * 0.01) * u_grainIntensity; + finalColor += vec3(grain); + + // Add some iridescence based on viewing angle + float iridescence = sin(fresnel * 3.14159 + u_time) * 0.1; + finalColor += vec3(iridescence * 0.5, iridescence * 0.8, iridescence); + + gl_FragColor = vec4(finalColor, 1.0); +} \ No newline at end of file diff --git a/app/components/grainy-liquid/shaders/fragment.glsl.d.ts b/app/components/grainy-liquid/shaders/fragment.glsl.d.ts new file mode 100644 index 00000000..f93db699 --- /dev/null +++ b/app/components/grainy-liquid/shaders/fragment.glsl.d.ts @@ -0,0 +1,2 @@ +declare const content: string +export default content \ No newline at end of file diff --git a/app/components/grainy-liquid/shaders/vertex.glsl b/app/components/grainy-liquid/shaders/vertex.glsl new file mode 100644 index 00000000..81fbb17d --- /dev/null +++ b/app/components/grainy-liquid/shaders/vertex.glsl @@ -0,0 +1,109 @@ +uniform float u_time; +uniform float u_amplitude; +uniform float u_frequency; +uniform float u_speed; + +varying vec3 v_position; +varying vec3 v_normal; +varying vec2 v_uv; + +// Simplex noise functions +vec3 mod289(vec3 x) { + return x - floor(x * (1.0 / 289.0)) * 289.0; +} + +vec4 mod289(vec4 x) { + return x - floor(x * (1.0 / 289.0)) * 289.0; +} + +vec4 permute(vec4 x) { + return mod289(((x*34.0)+1.0)*x); +} + +vec4 taylorInvSqrt(vec4 r) { + return 1.79284291400159 - 0.85373472095314 * r; +} + +float snoise(vec3 v) { + const vec2 C = vec2(1.0/6.0, 1.0/3.0) ; + const vec4 D = vec4(0.0, 0.5, 1.0, 2.0); + + vec3 i = floor(v + dot(v, C.yyy) ); + vec3 x0 = v - i + dot(i, C.xxx) ; + + vec3 g = step(x0.yzx, x0.xyz); + vec3 l = 1.0 - g; + vec3 i1 = min( g.xyz, l.zxy ); + vec3 i2 = max( g.xyz, l.zxy ); + + vec3 x1 = x0 - i1 + C.xxx; + vec3 x2 = x0 - i2 + C.yyy; + vec3 x3 = x0 - D.yyy; + + i = mod289(i); + vec4 p = permute( permute( permute( + i.z + vec4(0.0, i1.z, i2.z, 1.0 )) + + i.y + vec4(0.0, i1.y, i2.y, 1.0 )) + + i.x + vec4(0.0, i1.x, i2.x, 1.0 )); + + float n_ = 0.142857142857; + vec3 ns = n_ * D.wyz - D.xzx; + + vec4 j = p - 49.0 * floor(p * ns.z * ns.z); + + vec4 x_ = floor(j * ns.z); + vec4 y_ = floor(j - 7.0 * x_ ); + + vec4 x = x_ *ns.x + ns.yyyy; + vec4 y = y_ *ns.x + ns.yyyy; + vec4 h = 1.0 - abs(x) - abs(y); + + vec4 b0 = vec4( x.xy, y.xy ); + vec4 b1 = vec4( x.zw, y.zw ); + + vec4 s0 = floor(b0)*2.0 + 1.0; + vec4 s1 = floor(b1)*2.0 + 1.0; + vec4 sh = -step(h, vec4(0.0)); + + vec4 a0 = b0.xzyw + s0.xzyw*sh.xxyy ; + vec4 a1 = b1.xzyw + s1.xzyw*sh.zzww ; + + vec3 p0 = vec3(a0.xy,h.x); + vec3 p1 = vec3(a0.zw,h.y); + vec3 p2 = vec3(a1.xy,h.z); + vec3 p3 = vec3(a1.zw,h.w); + + vec4 norm = taylorInvSqrt(vec4(dot(p0,p0), dot(p1,p1), dot(p2, p2), dot(p3,p3))); + p0 *= norm.x; + p1 *= norm.y; + p2 *= norm.z; + p3 *= norm.w; + + vec4 m = max(0.6 - vec4(dot(x0,x0), dot(x1,x1), dot(x2,x2), dot(x3,x3)), 0.0); + m = m * m; + return 42.0 * dot( m*m, vec4( dot(p0,x0), dot(p1,x1), + dot(p2,x2), dot(p3,x3) ) ); +} + +void main() { + v_uv = uv; + v_normal = normal; + + vec3 pos = position; + float time = u_time * u_speed; + + // Create multiple layers of noise for liquid-like movement + float noise1 = snoise(pos * u_frequency + time * 0.5); + float noise2 = snoise(pos * u_frequency * 2.0 + time * 0.3); + float noise3 = snoise(pos * u_frequency * 4.0 + time * 0.7); + + // Combine noise layers with different amplitudes + float displacement = noise1 * 0.6 + noise2 * 0.3 + noise3 * 0.1; + + // Apply displacement along normal direction with smoother transition + vec3 displacedPosition = pos + normal * displacement * u_amplitude; + + v_position = displacedPosition; + + gl_Position = projectionMatrix * modelViewMatrix * vec4(displacedPosition, 1.0); +} \ No newline at end of file diff --git a/app/components/grainy-liquid/shaders/vertex.glsl.d.ts b/app/components/grainy-liquid/shaders/vertex.glsl.d.ts new file mode 100644 index 00000000..f93db699 --- /dev/null +++ b/app/components/grainy-liquid/shaders/vertex.glsl.d.ts @@ -0,0 +1,2 @@ +declare const content: string +export default content \ No newline at end of file diff --git a/content/experiments/grainy-liquid.md b/content/experiments/grainy-liquid.md new file mode 100644 index 00000000..bb9c843b --- /dev/null +++ b/content/experiments/grainy-liquid.md @@ -0,0 +1,7 @@ +--- +title: Grainy Liquid +author: alvarosabu +description: A grainy liquid blob +tags: ['shaders'] +date: 2025-08-12 +--- From 424834bae305b6f53191b05be28403456f223091 Mon Sep 17 00:00:00 2001 From: alvarosabu Date: Tue, 12 Aug 2025 22:34:09 +0200 Subject: [PATCH 2/4] feat: introduce MultiBlob component for enhanced grainy liquid effect - Added `MultiBlob.vue` to render multiple `Blob` instances with configurable properties for position, scale, and color, enhancing the visual complexity of the grainy liquid effect. - Updated `Blob.vue` to accept props for color and animation parameters, allowing for dynamic customization. - Refactored `Experience.vue` to utilize `MultiBlob` instead of a single `Blob`, improving scene depth and interaction. - Created `WebsiteLayout.vue` to structure the main layout, integrating the 3D canvas and content overlay for a cohesive user experience. - Enhanced fragment shader to improve the grain effect, adding more texture and depth to the visual output. These changes significantly enhance the visual richness and interactivity of the grainy liquid experience, providing users with a more engaging and dynamic interface. --- app/components/grainy-liquid/Blob.vue | 34 +++++-- app/components/grainy-liquid/Experience.vue | 15 ++-- app/components/grainy-liquid/MultiBlob.vue | 62 +++++++++++++ .../grainy-liquid/WebsiteLayout.vue | 88 +++++++++++++++++++ app/components/grainy-liquid/index.global.vue | 7 +- .../grainy-liquid/shaders/fragment.glsl | 9 +- 6 files changed, 192 insertions(+), 23 deletions(-) create mode 100644 app/components/grainy-liquid/MultiBlob.vue create mode 100644 app/components/grainy-liquid/WebsiteLayout.vue diff --git a/app/components/grainy-liquid/Blob.vue b/app/components/grainy-liquid/Blob.vue index b707464e..53e7ea8c 100644 --- a/app/components/grainy-liquid/Blob.vue +++ b/app/components/grainy-liquid/Blob.vue @@ -4,13 +4,29 @@ import { Vector3, Color } from 'three' import vertexShader from './shaders/vertex.glsl' import fragmentShader from './shaders/fragment.glsl' +interface Props { + colorA?: string + colorB?: string + colorC?: string + speed?: number + amplitude?: number +} + +const props = withDefaults(defineProps(), { + colorA: '#6366f1', + colorB: '#ec4899', + colorC: '#f1f5f9', + speed: 0.8, + amplitude: 0.15 +}) + const { onBeforeRender } = useLoop() const blobRef = ref(null) -const { amplitude, frequency, speed, colorA, colorB, colorC, noiseScale, grainIntensity, fresnelPower } = useControls({ +const controlsConfig = { amplitude: { - value: 0.15, + value: props.amplitude, min: 0, max: 0.5, step: 0.01, @@ -24,15 +40,15 @@ const { amplitude, frequency, speed, colorA, colorB, colorC, noiseScale, grainIn label: 'Frequency' }, speed: { - value: 0.8, + value: props.speed, min: 0.1, max: 2, step: 0.1, label: 'Speed' }, - colorA: '#1a66cc', - colorB: '#cc3366', - colorC: '#e6e6e6', + colorA: props.colorA, + colorB: props.colorB, + colorC: props.colorC, noiseScale: { value: 2.0, min: 0.5, @@ -41,7 +57,7 @@ const { amplitude, frequency, speed, colorA, colorB, colorC, noiseScale, grainIn label: 'Noise Scale' }, grainIntensity: { - value: 0.05, + value: 0.08, min: 0, max: 0.2, step: 0.01, @@ -54,7 +70,9 @@ const { amplitude, frequency, speed, colorA, colorB, colorC, noiseScale, grainIn step: 0.1, label: 'Fresnel' } -}) +} + +const { amplitude, frequency, speed, colorA, colorB, colorC, noiseScale, grainIntensity, fresnelPower } = useControls(controlsConfig) function hexToVector3(hex: string): Vector3 { const color = new Color(hex) diff --git a/app/components/grainy-liquid/Experience.vue b/app/components/grainy-liquid/Experience.vue index 5b76cd25..b83f0b63 100644 --- a/app/components/grainy-liquid/Experience.vue +++ b/app/components/grainy-liquid/Experience.vue @@ -1,23 +1,22 @@