Rim shaders on flat discs - edge chromatic effect optimization

import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';

// Scene setup
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true }); // Alpha for transparent background
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setClearColor(0x000000, 0); // Transparent background
document.body.appendChild(renderer.domElement);

// Camera controls
const controls = new OrbitControls(camera, renderer.domElement);
camera.position.set(0, 2, 15); // Adjusted for face-on view
controls.update();

// Custom shader material
const discMaterial = new THREE.ShaderMaterial({
    uniforms: {
        baseColor: { value: new THREE.Color(0x000015) }, // Nearly black navy blue
        edgeIntensity: { value: 5.0 } // Increased for vibrant edges
    },
    vertexShader: `
        varying vec3 vNormal;
        varying vec3 vViewPosition;
        varying vec2 vUv;
        
        void main() {
            vNormal = normalize(normalMatrix * normal);
            vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
            vViewPosition = -mvPosition.xyz;
            vUv = uv;
            gl_Position = projectionMatrix * mvPosition;
        }
    `,
    fragmentShader: `
        uniform vec3 baseColor;
        uniform float edgeIntensity;
        
        varying vec3 vNormal;
        varying vec3 vViewPosition;
        varying vec2 vUv;
        
        // Function to create a rainbow gradient
        vec3 getRainbowColor(float t) {
            vec3 magenta = vec3(1.0, 0.0, 1.0);
            vec3 purple = vec3(0.5, 0.0, 1.0);
            vec3 blue = vec3(0.0, 0.0, 1.0);
            vec3 yellow = vec3(1.0, 1.0, 0.0);
            vec3 teal = vec3(0.0, 1.0, 1.0);
            
            if (t < 0.25) return mix(magenta, purple, t / 0.25);
            else if (t < 0.5) return mix(purple, blue, (t - 0.25) / 0.25);
            else if (t < 0.75) return mix(blue, yellow, (t - 0.5) / 0.25);
            else return mix(yellow, teal, (t - 0.75) / 0.25);
        }
        
        void main() {
            vec3 normal = normalize(vNormal);
            vec3 viewDir = normalize(vViewPosition);
            
            // Edge detection
            float edgeFactor = 1.0 - abs(dot(normal, viewDir));
            float edge = smoothstep(0.95, 0.98, edgeFactor); // Wider edge range
            
            // Edge gradient based on distance from center
            float dist = length(vUv - 0.5);
            float edgeGradient = smoothstep(0.45, 0.5, dist); // Wider rim for visibility
            
            // Apply rainbow gradient to the edge
            float angle = atan(vUv.x - 0.5, vUv.y - 0.5) / (2.0 * 3.14159) + 0.5;
            vec3 rainbowColor = getRainbowColor(angle);
            vec3 finalEdge = rainbowColor * edgeGradient * edge * edgeIntensity;
            
            // Mix base color with edge, prioritizing edge color
            vec3 finalColor = mix(baseColor, finalEdge, edgeGradient * 2.0);
            
            gl_FragColor = vec4(finalColor, 1.0);
        }
    `,
    side: THREE.DoubleSide
});

// Create three discs in a triangular arrangement
function createDiscs() {
    const discGroup = new THREE.Group();
    const geometry = new THREE.CylinderGeometry(2, 2, 0.4, 128); // Increased segments for smoothness
    
    const positions = [
        [-4, 0, 0],  // Bottom left
        [4, 0, 0],   // Bottom right
        [0, 4, 0]    // Top center
    ];

    positions.forEach(pos => {
        const disc = new THREE.Mesh(geometry, discMaterial);
        disc.position.set(...pos);
        disc.rotation.set(0.05, 0.05, 0.05); // Reduced rotation
        discGroup.add(disc);
    });
    
    return discGroup;
}

const discs = createDiscs();
scene.add(discs);

// Lighting
const ambientLight = new THREE.AmbientLight(0x505050, 0.8); // Stronger ambient light
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 2.0); // Stronger directional light
directionalLight.position.set(0, 10, 10);
scene.add(directionalLight);
const directionalLight2 = new THREE.DirectionalLight(0xffffff, 1.5); // Enhanced edge light
directionalLight2.position.set(10, 10, 0);
scene.add(directionalLight2);

// Animation loop
function animate() {
    requestAnimationFrame(animate);
    controls.update();
    renderer.render(scene, camera);
}
animate();

// Handle window resize
window.addEventListener('resize', () => {
    camera.aspect = window.innerWidth / window.innerHeight;
    camera.updateProjectionMatrix();
    renderer.setSize(window.innerWidth, window.innerHeight);
});

I’m working on creating flat disc objects with an ultra-smooth dark surface and iridescent edge effects similar to the reference image below (imagine 3 floating discs with navy centers and vibrant rainbow-colored edges).

Current implementation details:

  • Using flat circular geometries with minimal thickness
  • Dark navy/black central surface using MeshStandardMaterial
  • Attempting to create a chromatic edge effect that displays magenta, purple, blue, yellow, and teal gradients
  • PBR rendering pipeline

Technical challenges:

  1. Getting the edge shader to properly display the holographic color gradient without affecting the dark center
  2. Maintaining the metallic reflective quality on the edges while keeping the center matte
  3. Optimizing the rim effect for different viewing angles

Has anyone successfully implemented similar edge-highlighting effects? Would a custom shader be more efficient than trying to fake this with material combinations? Any code examples or techniques would be greatly appreciated.

Looking specifically for implementations that maintain good performance while keeping the visual fidelity of the iridescent edges when the camera position changes.

Example attached

Lots of ways to approach this.. depending on time and constraints. You could simulate it with standard material and an environment map (or a few different lights…)
the image has some β€œanisotropic” properties on the rim.. like brushed metal.. you could use a brushed metal normalmap or texture to simulate that.
Alternatively you could hack the StandardMaterial shader, or make a completely custom shader to simulate that look. defnitely not beginner stuff tho.

1 Like

Thanks for the quick reply! Will do some research on those points. I like jumping into the deep end when learning new tech. If I get stuck with the environment maps or anisotropic effects, I might take a shot at modifying the shader

1 Like

The same thought :slight_smile:

Demo: https://codepen.io/prisoner849/full/MYWQwxY

6 Likes

That was fast and looks great! Would you mind sharing that example with me? Still new to this all.

Added the link, below the picture. :handshake:

2 Likes

Thanks a bunch, diving into your code now. Appreciate the help!

1 Like