Chromatic Ferrofluid Portfolio With React And GLSL
Let's dive into creating a cutting-edge portfolio frontend that pushes the boundaries of web browser capabilities. Inspired by "Alien Technology" and "Liquid Mathematics," this design will focus on a mesmerizing "Chromatic Ferrofluid Refraction" effect. We'll be leveraging React, React Three Fiber (R3F), and GLSL shaders to achieve a visually stunning and mathematically complex result. Forget about performance limitations; our goal is maximum visual fidelity.
Design Concept: Chromatic Ferrofluid Refraction
Imagine a vat of liquid chrome or intelligent liquid metal, morphing and twisting in real time. That's the essence of our design. This isn't your typical glassmorphism or space stars portfolio; we're aiming for something unique and groundbreaking. The core will be a raymarching scene using Signed Distance Functions (SDFs) to render a morphing, reflective blob that feels both organic and otherworldly.
The Visual Elements
- Morphing Blob: At the heart of our design is a dynamic, shape-shifting blob created using SDFs. Think Mandelbulb or Gyroid fractal interpolation for intricate, self-folding forms.
- Reflective Metallic Surface: The material will mimic a high-index refraction material, like glass or diamond mixed with liquid mercury. This will allow the blob to refract the background and reflect its own complex geometry, creating a mesmerizing visual effect.
- Procedural Lighting: We'll generate Image Based Lighting (IBL) procedurally via code, ensuring our lighting is perfectly integrated with the scene. To add another layer of visual depth, we'll incorporate volumetric god-rays, simulating light scattering through the fluid.
Technical Requirements: The God-Level Stack
We're not holding back on the technology. This project will utilize a powerful stack to achieve its ambitious visual goals.
- Framework: React + React Three Fiber (R3F). React will provide the structure and interactivity, while R3F will seamlessly integrate Three.js for 3D rendering.
- The Heavy Lifting: Custom
shaderMaterial: The star of the show will be a customshaderMaterialthat implements a full raymarching loop directly in the fragment shader. This gives us unparalleled control over the rendering process and allows for complex visual effects. This material must include the rayMarch(), sdfScene(), and calcNormal() functions. - Advanced Shader Techniques: We'll implement soft shadows and ambient occlusion purely through mathematical calculations within the shader, avoiding traditional rendering techniques for maximum flexibility and visual fidelity.
- Post-Processing with
@react-three/postprocessing: To further enhance the visual quality, we'll employ a chain of post-processing effects, including Bloom (high intensity), Chromatic Aberration (reactive to mouse speed), Noise, and SMAA (Antialiasing). - Dynamic Interaction: The SDF shape should mutate in response to mouse position, creating a tactile and engaging experience. We'll also add subtle "glitch" effects on text elements when hovered over, injecting a touch of digital chaos.
Dependencies
To get started, you'll need the following packages:
reactreact-dom@react-three/fiberthree@react-three/drei@react-three/postprocessingglslify-cliglsl-noisesimplex-noise
Install these dependencies using your preferred package manager (npm or yarn).
npm install react react-dom @react-three/fiber three @react-three/drei @react-three/postprocessing glslify-cli glsl-noise simplex-noise
The Shader: GLSL Code for the Raymarching SDF Scene
This is where the magic happens. The following GLSL code implements the raymarching algorithm, SDF scene, lighting calculations, and material properties. Remember, we're aiming for maximum visual fidelity and mathematical complexity, so no simplification here!
Create a file named chromaticFerrofluid.glsl (or similar) and paste the following code:
#pragma glslify: snoise3 = require(glsl-noise/simplex/3d)
#pragma glslify: snoise4 = require(glsl-noise/simplex/4d)
uniform float uTime;
uniform vec2 uMouse;
uniform vec2 resolution;
// Configuration
#define MAX_STEPS 256
#define MAX_DIST 100.0
#define SURF_DIST 0.001
// SDF Functions
float sdSphere(vec3 p, float r) {
return length(p) - r;
}
float sdBox(vec3 p, vec3 b) {
vec3 d = abs(p) - b;
return min(max(d.x,max(d.y,d.z)),0.0) +
length(max(d,0.0));
}
float opU( float d1, float d2 ) {
return min(d1,d2);
}
// SDF Scene (Mandelbulb Fractal Interpolation)
float sdfScene(vec3 p) {
// Mandelbulb parameters
float bailout = 10.0;
float power = 8.0;
// Complex number representation
vec4 z = vec4(p, 0.0);
float dr = 1.0;
float r;
for (int i = 0; i < 16; i++) {
r = length(z.xyz);
if (r > bailout) break;
// Calculate derivatives
float theta = atan(length(z.xz), z.y) * power;
float phi = atan(z.x, z.z) * power;
float zr = pow(r, power - 1.0);
dr = pow(r, power - 1.0) * power * dr + 1.0;
// "Fold" the complex number
float znewx = zr * sin(theta) * cos(phi);
float znewy = zr * cos(theta);
float znewz = zr * sin(theta) * sin(phi);
z = vec4(znewx, znewy, znewz, 0.0) + vec4(p, 0.0);
}
float mandelbulbDist = 0.5 * log(r) * r / dr;
// Gyroid parameters
float gyroidScale = 2.0;
vec3 gyroidP = p * gyroidScale;
float gyroidDist = abs(dot(sin(gyroidP), cos(vec3(gyroidP.yzx, gyroidP.zxy))));
float gyroidOffset = 0.2;
// Mouse interaction influence
float mouseInfluence = smoothstep(0.2, 0.5, length(uMouse - gl_FragCoord.xy / resolution));
float timeOffset = uTime * 0.2 + snoise3(p * 0.5 + vec3(uTime * 0.1)); // Time-based noise
// Interpolate between Mandelbulb and Gyroid
float blendFactor = 0.6 + 0.4 * sin(uTime * 0.15 + snoise3(p * 0.3)) + mouseInfluence * 0.8;
return mix(mandelbulbDist, gyroidDist + gyroidOffset, blendFactor);
}
// Ray Marching
float rayMarch(vec3 ro, vec3 rd) {
float dO=0.0;
for (int i=0; i<MAX_STEPS; i++) {
vec3 p = ro + rd*dO;
float dS = sdfScene(p);
dO += dS;
if(dO>MAX_DIST || dS<SURF_DIST) break;
}
return dO;
}
// Calculate Normal
vec3 calcNormal(vec3 p) {
vec2 e = vec2(SURF_DIST, 0.0);
return normalize(vec3(
sdfScene(p + e.xyy) - sdfScene(p - e.xyy),
sdfScene(p + e.yxy) - sdfScene(p - e.yxy),
sdfScene(p + e.yyx) - sdfScene(p - e.yyx)
));
}
// Soft Shadows
float softShadow(vec3 ro, vec3 rd, float k) {
float res = 1.0;
float t = SURF_DIST;
for (int i = 0; i < 32; i++) {
float h = sdfScene(ro + rd * t);
if (h < SURF_DIST) {
return 0.0;
}
res = min(res, k * h / t);
t += h;
}
return res;
}
// Ambient Occlusion
float ambientOcclusion(vec3 p, vec3 n) {
float occ = 0.0;
float sca = 1.0;
for (int i = 0; i < 5; i++) {
float hr = SURF_DIST + float(i) / 4.0;
vec3 aopos = n * hr + p;
float dd = sdfScene(aopos);
occ += (hr - dd) * sca;
sca *= 0.95;
}
return clamp(1.0 - 0.3 * occ, 0.0, 1.0);
}
// Procedural IBL
vec3 generateIBL(vec3 normal) {
// Very basic IBL generation (can be expanded)
vec3 lightDir1 = normalize(vec3(0.5, 1.0, 0.8));
vec3 lightColor1 = vec3(1.0, 0.9, 0.8);
vec3 lightDir2 = normalize(vec3(-0.8, 0.5, -0.3));
vec3 lightColor2 = vec3(0.7, 0.8, 1.0);
float diffuse1 = max(dot(normal, lightDir1), 0.0);
float diffuse2 = max(dot(normal, lightDir2), 0.0);
return lightColor1 * diffuse1 + lightColor2 * diffuse2;
}
// Volumetric God-Rays
vec3 volumetricGodRays(vec3 ro, vec3 rd) {
vec3 lightDir = normalize(vec3(0.5, 1.0, 0.8));
float density = 0.0;
float weight = 0.05;
for (int i = 0; i < 64; i++) {
vec3 p = ro + rd * float(i) * 0.1;
float lightDot = max(dot(-lightDir, normalize(p)), 0.0);
density += weight * pow(lightDot, 16.0);
}
return vec3(density) * vec3(0.8, 0.9, 1.0);
}
void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
vec2 uv = (fragCoord - 0.5 * resolution.xy) / resolution.y;
// Camera Setup
vec3 ro = vec3(0, 0, 4.0);
vec3 lookAt = vec3(0);
vec3 rd = normalize(vec3(uv, -2.0));
// Mouse Interaction: Rotate camera
float angleX = (uMouse.x - 0.5) * PI * 0.5;
float angleY = (uMouse.y - 0.5) * PI * 0.5;
// Rotation matrices
mat3 rotX = mat3(
1.0, 0.0, 0.0,
0.0, cos(angleY), -sin(angleY),
0.0, sin(angleY), cos(angleY)
);
mat3 rotY = mat3(
cos(angleX), 0.0, sin(angleX),
0.0, 1.0, 0.0,
-sin(angleX), 0.0, cos(angleX)
);
rd = rotX * rotY * rd; // Apply rotations
// Raymarch
float dist = rayMarch(ro, rd);
vec3 p = ro + rd * dist;
vec3 normal = calcNormal(p);
// Lighting
vec3 ibl = generateIBL(normal);
float shadow = softShadow(p, normalize(vec3(0.5, 1.0, 0.8)), 8.0);
float ao = ambientOcclusion(p, normal);
vec3 godRays = volumetricGodRays(ro, rd);
// Material properties
vec3 reflectionDir = reflect(rd, normal);
vec3 reflectedColor = generateIBL(reflectionDir);
float fresnel = pow(1.0 + dot(rd, normal), 3.0);
vec3 baseColor = vec3(0.1, 0.3, 0.2); // Muted chromatic base
vec3 metallicColor = mix(baseColor, reflectedColor, fresnel);
vec3 lightColor = ibl * shadow * ao + godRays;
vec3 finalColor = metallicColor * lightColor;
// Chromatic Aberration (exaggerated for effect)
float aberrationAmount = length(uMouse - vec2(0.5)) * 0.1; // Mouse speed
vec3 redShift = rayMarch(ro, rd + vec3(aberrationAmount * 0.01, 0, 0)) * rd.xyz; // Shift red channel
vec3 blueShift = rayMarch(ro, rd - vec3(aberrationAmount * 0.01, 0, 0)) * rd.xyz; // Shift blue channel
finalColor.r += redShift.x * 0.1;
finalColor.b += blueShift.z * 0.1;
fragColor = vec4(finalColor, 1.0);
}
void main() {
mainImage(gl_FragColor, gl_FragCoord);
}
This shader code defines the core logic for rendering the chromatic ferrofluid effect. It includes:
- SDF functions:
sdSphere,sdBox, andopUfor basic shape definitions. sdfScene: Combines a Mandelbulb fractal and a Gyroid using interpolation, creating the complex morphing shape. It also incorporates mouse interaction to influence the shape.rayMarch: Implements the raymarching algorithm to find the intersection point of the ray with the scene.calcNormal: Calculates the surface normal at a given point.softShadow: Generates soft shadows using a mathematical approximation.ambientOcclusion: Calculates ambient occlusion to add depth and realism.generateIBL: Generates a basic Image-Based Lighting environment procedurally.volumetricGodRays: Adds volumetric god-rays for atmospheric lighting effects.mainImage: The main function that sets up the camera, ray direction, lighting, material properties, and post-processing effects, including chromatic aberration.
Remember: This is a complex shader, and it's designed to push the limits. Performance may vary depending on the hardware.
The Scene: R3F Setup
Now, let's set up the React Three Fiber scene to use this shader. Create a React component (e.g., FerrofluidScene.js) and paste the following code:
import React, { useRef, useMemo } from 'react';
import { Canvas, useFrame } from '@react-three/fiber';
import { useAspect, } from '@react-three/drei'
import { EffectComposer, Bloom, ChromaticAberration, Noise, SMAA } from '@react-three/postprocessing'
import { Uniform } from 'three';
import { useControls } from 'leva'
// Import the shader (ensure glslify-cli is set up to transform .glsl files)
import chromaticFerrofluidShader from './chromaticFerrofluid.glsl';
// Custom Shader Material
const ChromaticFerrofluidMaterial = ({ time, mouse }) => {
const material = useRef();
const resolution = useAspect('fit')
useFrame(({ clock }) => {
if (material.current) {
material.current.uniforms.uTime.value = clock.getElapsedTime();
material.current.uniforms.uMouse.value = mouse;
material.current.uniforms.resolution.value.set(resolution.width, resolution.height)
}
});
const [ mat ] = useMemo(() => {
const mat = new THREE.ShaderMaterial({
vertexShader: /* glsl */`
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`,
fragmentShader: chromaticFerrofluidShader,
uniforms: {
uTime: { value: 0 },
uMouse: { value: { x: 0, y: 0 } },
resolution: { value: new THREE.Vector2() },
},
});
return [ mat ]
}, [])
return <shaderMaterial key={mat.uuid} ref={material} material={mat} />;
};
// Main Scene Component
const FerrofluidScene = () => {
const mouse = useRef({ x: 0, y: 0 });
const { mouseSpeed } = useControls("Chromatic Aberration", { mouseSpeed: { value: 0, min: 0, max: 0.1, step: 0.001 } })
const onMouseMove = (e) => {
mouse.current = {
x: e.clientX / window.innerWidth,
y: 1 - e.clientY / window.innerHeight,
};
};
return (
<div style={{ width: '100vw', height: '100vh', overflow: 'hidden' }} onMouseMove={onMouseMove}>
<Canvas dpr={[1, 2]} camera={{ fov: 75, position: [0, 0, 4] }}>
<mesh>
<sphereGeometry args={[1, 256, 256]} />
<ChromaticFerrofluidMaterial mouse={mouse.current} />
</mesh>
<EffectComposer multisampling={8}>
<Bloom intensity={1.5} luminanceThreshold={0.4} luminanceSmoothing={0.9} />
<ChromaticAberration offset={[mouse.current.x* mouseSpeed, mouse.current.y* mouseSpeed]} />
<Noise opacity={0.05} />
<SMAA />
</EffectComposer>
</Canvas>
</div>
);
};
export default FerrofluidScene;
Key aspects of this component:
- Imports: Imports necessary modules from React, R3F, Drei, and Postprocessing.
ChromaticFerrofluidMaterial: This component creates the custom shader material, passes uniforms (time, mouse position, resolution) to the shader, and updates them on each frame usinguseFrame. The shader is imported aschromaticFerrofluidShader.- Uniform Updates: Inside the
useFramehook, theuTime,uMouse, andresolutionuniforms are updated, driving the animation and interaction. FerrofluidScene: The main component sets up the R3F canvas, creates a sphere geometry, and applies theChromaticFerrofluidMaterial. It also includes post-processing effects for bloom, chromatic aberration, noise, and antialiasing. Mouse movement is tracked and passed as a uniform to the shader.- Post-Processing: The
<EffectComposer>component chains together post-processing effects to enhance the visual output. Bloom adds a glow effect, Chromatic Aberration simulates lens distortion, Noise adds a subtle grain, and SMAA provides antialiasing. - leva useControls to control the mouseSpeed value.
Setting up GLSL Transformation
To use GLSL shaders in React, you need to transform them into JavaScript strings. glslify-cli is the great tool for the job, Add the following script to your package.json:
"scripts": {
"glslify": "glslify src/**/*.glsl -o src/shaders.js",
...
},
Integrating into Your App
Finally, integrate the FerrofluidScene component into your main App component or any other part of your application:
import React from 'react';
import FerrofluidScene from './FerrofluidScene';
function App() {
return (
<div className="App">
<FerrofluidScene />
</div>
);
}
export default App;
Pushing the Boundaries
This setup provides a solid foundation for a visually stunning and technically impressive portfolio frontend. The chromatic ferrofluid effect, driven by a complex GLSL shader and enhanced with post-processing, creates a unique and memorable experience.
Remember: The key to achieving the "Alien Technology" or "Liquid Mathematics" aesthetic is to embrace complexity and push the limits of what's possible. Don't be afraid to experiment with different SDF combinations, lighting techniques, and post-processing effects to create your own unique visual signature.
This is just a starting point. You can further enhance the design by:
- Adding more intricate SDF shapes and animations.
- Implementing more advanced lighting and shading techniques.
- Incorporating user interactions to control the ferrofluid simulation.
- Integrating portfolio content and navigation elements seamlessly into the 3D scene.
By embracing the power of React, R3F, and GLSL shaders, you can create a portfolio that truly stands out and showcases your technical and creative skills. If you're interested in further exploring raymarching techniques and GLSL shaders, I highly recommend checking out The Book of Shaders for in-depth knowledge and inspiration.