Search code examples
three.jsglslshaderfragment-shadervertex-shader

How can I project a 2d shader in a 3d object


the title make it looks easy but I'm struggling to get this pie chart shader on my 3d model. For this I'm using three js. here is my code till now:

index.html:

<!DOCTYPE html>
<html>

<head>
    <meta charset='utf-8'>
    <meta http-equiv='X-UA-Compatible' content='IE=edge'>
    <title>Site Prof Matheus</title>
    <meta name='viewport' content='width=device-width, initial-scale=1'>
    <link rel='stylesheet' type='text/css' media='screen' href='./styles/main.css'>
    <script type='module' src='./src/main.js'></script>

</head>

<body>

</body>

</html>

main.js:

import * as THREE from 'https://cdn.skypack.dev/three'
import { OrbitControls } from 'https://cdn.skypack.dev/three-stdlib/controls/OrbitControls'
import { GLTFLoader } from 'https://cdn.skypack.dev/three-stdlib/loaders/GLTFLoader'
import { vertexShader } from '../shaders/vertex.glsl.js'
import { fragmentShader } from '../shaders/fragment.glsl.js'

const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
const loader = new GLTFLoader();
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setClearColor(0x002653, 0.5)
renderer.setSize(window.innerWidth, window.innerHeight);

document.body.appendChild(renderer.domElement);
renderer.setPixelRatio(window.devicePixelRatio);

const r_material = new THREE.ShaderMaterial({ //Roulette material
    uniforms: {
        iTime: { value: 1.0 },
        resolution: { value: new THREE.Vector2 }
    },
    vertexShader,
    fragmentShader
})
loader.load(
    '../roulette.glb',
    function (gltf) {
        gltf.scene.traverse((o) => {
            if (o.isMesh) o.material = r_material;
        });
        scene.add(gltf.scene);

    },
    function () { },
    function (err) {
        console.log('error: ' + err);
    }

)
camera.position.z = 5;

const controls = new OrbitControls(camera, renderer.domElement);
controls.enableZoom = true;
controls.enableDamping = true;

function animate() {
    requestAnimationFrame(animate);

    controls.update();
    renderer.render(scene, camera);
};

animate();

vertex.glsl.js:

const vertexShader = /* glsl */`
void main()
{
    gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
}
`
// uniform vec3 iResolution;
// uniform int SEGMENTCOUNT;
export { vertexShader };

fragment.glsl.js:

const fragmentShader = /* glsl */`
void main() {
    gl_FragColor = vec4(0, 1, 0, 1);
    gl_Position
}
`

export { fragmentShader };

and here is my roulette.gbl model

the intended result is to have a shader with colors that I choose, all the parts equal, the colors can repeat and the shader should covers the whole mesh, less the bottom. intended result

PS. i see my object looks a flat plain on the image, i guess just need to add some proper light, here is my object geometry just for sake of curiosity: my mesh geometry

PSS. some antialiasing on the pie chart shader would be very welcome


Solution

  • This is how you can do it, modifying a material with .onBeforeCompile():

    enter image description here

    body{
      overflow: hidden;
      margin: 0;
    }
    <script type="module">
    import * as THREE from "https://cdn.skypack.dev/[email protected]";
    import {
      OrbitControls
    } from "https://cdn.skypack.dev/[email protected]/examples/jsm/controls/OrbitControls";
    
    let scene = new THREE.Scene();
    let camera = new THREE.PerspectiveCamera(60, innerWidth / innerHeight, 1, 2000);
    camera.position.set(0, 1, 1).setLength(12);
    let renderer = new THREE.WebGLRenderer({
      antialias: true
    });
    renderer.setSize(innerWidth, innerHeight);
    document.body.appendChild(renderer.domElement);
    
    window.addEventListener("resize", onWindowResize);
    
    let controls = new OrbitControls(camera, renderer.domElement);
    
    scene.add(new THREE.GridHelper());
    
    let light = new THREE.DirectionalLight(0xffffff, 0.5);
    light.position.setScalar(1);
    scene.add(light, new THREE.AmbientLight(0xffffff, 0.5));
    
    let path = new THREE.Path();
    path.moveTo(0, -1);
    path.lineTo(4, -1);
    path.absarc(4, -0.5, 0.5, Math.PI * 1.5, 0);
    path.absarc(4, 0.5, 0.5, 0, Math.PI * 0.5);
    path.lineTo(0, -0.5);
    
    let g = new THREE.LatheGeometry(path.getPoints(50), 72);
    let m = new THREE.MeshLambertMaterial({
      color: 0x00ff7f,
      //wireframe: true,
      onBeforeCompile: shader => {
        shader.vertexShader = `
            varying vec3 vPos;
          ${shader.vertexShader}
        `.replace(
          `#include <begin_vertex>`,
          `#include <begin_vertex>
            vPos = position;
          `
        );
        //console.log(shader.vertexShader);
        shader.fragmentShader = `
            #define ss(a, b, c) smoothstep(a, b, c)
            varying vec3 vPos;
          ${shader.fragmentShader}
        `.replace(
          `vec4 diffuseColor = vec4( diffuse, opacity );`,
          `
          vec3 col = diffuse;
          int N = 37;
          float a = atan(vPos.x,-vPos.z)+PI;
          float r = PI2/float(N);
          float cId = floor(a/r);
          
          vec3 br = mod(cId, 2.) == 0. ? vec3(0) : vec3(1, 0, 0); // black / red
          br = cId == 0. ? vec3(0, 0.75, 0) : br; // green
          
          float d = length(vPos.xz);
          float fw = length(fwidth(vPos.xz));
          
          col = mix(col, br, ss(3. - fw, 3., d) - ss(4., 4. + fw, d));
          col = mix(diffuse, col, clamp(sign(vPos.y), 0., 1.));
          
          vec4 diffuseColor = vec4( col, opacity );
          `
        );
        //console.log(shader.fragmentShader);
      }
    })
    let o = new THREE.Mesh(g, m);
    o.position.y = 0.5;
    o.rotation.y = Math.PI;
    scene.add(o);
    
    renderer.setAnimationLoop(() => {
      renderer.render(scene, camera);
    });
    
    function onWindowResize() {
    
      camera.aspect = innerWidth / innerHeight;
      camera.updateProjectionMatrix();
    
      renderer.setSize(innerWidth, innerHeight);
    
    }
    
    </script>