A dynamically deformable circle (+shader)

The default circle of three.js (CircleGeometry) has one vertex in the center point and the others at the edge.

If you want to animate the circle area, you need much more vertices.

CircleDynamicallyFormable



// inputs
const geometry = new THREE.BufferGeometry( );
const radius = 0.96;
const radiusFunction = ( r, theta, t ) => r * ( 1 + 0.03 * Math.sin( theta * 18 ) * Math.cos( t ) );  
const heightFunction = ( n, t ) => 0.04 * ( 1 + Math.sin( Math.PI * n * 12 ) ) * Math.cos( 0.3 * t );
const rings = 144;
const parts = 6;

CircleCustom( geometry, radius, radiusFunction, heightFunction, rings, parts );


function CircleCustom( g, r, rf, hf, rings, parts ) {
    
    const vertexCount = 1 + parts / 2 * rings * ( rings + 1 ) ;
    
    let idxCount = 0;	
    let faceCount = parts * rings * rings;
    
    const faceIndices = new Uint32Array( faceCount * 3 );
    const vertices = new Float32Array( vertexCount * 3 );  
    const uvs = new Float32Array( vertexCount * 2 );
       
    g.setIndex( new THREE.BufferAttribute( faceIndices, 1 ) );	
    g.setAttribute( 'position', new THREE.BufferAttribute( vertices, 3 ).setUsage( THREE.DynamicDrawUsage) );
    g.setAttribute( 'uv', new THREE.BufferAttribute( uvs, 2 ) );
    
    const setFace =  ( ) => {
        
        faceIndices[ idxCount     ] = a;
        faceIndices[ idxCount + 1 ] = b;
        faceIndices[ idxCount + 2 ] = c; 
            
        idxCount += 3;
        
    }
    
    let posIdx, uvIdx;
    
    let a = 0; // vertex 0: center
    let b = 1;
    let c = 2;
    
    for ( let j = 0; j < parts; j ++ ) { // around center
    
        setFace( );	
        b ++;
        if ( b < parts ) { c ++; } else { c = 1; }
    
    }
    
    let rvSum = 1; // only vertex 0
    
    for ( let i = 1; i < rings; i ++ ) {
        
        for ( let q = 0; q < parts; q ++ ) {
            
            for ( let j = 0; j < i + 1 ; j ++ ) {  
                                
                if ( j === 0 ) {
                
                    //  first face in part 
                    
                    a = rvSum;
                    b = a + parts * i + q;
                    c = b + 1;
                    
                    setFace();
                    
                } else {
                     
                    //  two faces / vertex
                    
                    a = j + rvSum; 
                    b = a - 1; 
                    c = a + parts * i + q;
                    if ( q === ( parts - 1 ) && j === i ) a = a - parts * i; // connect to first vertex of circle
                    
                    setFace();
                    
                    // a  from first face 
                    b = c; // from first face
                    c = b + 1;
                    
                    if ( q === ( parts - 1 ) && j === i ) c = c - parts * ( i + 1 ); // connect to first vertex of next circle
                    
                    setFace();
                    
                }
                
            }
            
            rvSum += i;
            
        }
        
    }   
    
    uvs[ 0 ] = 0.5;
    uvs[ 1 ] = 0.5;
    
    let u, v;
                
    rvSum = 1;  // without center
    
    for ( let i = 0; i <= rings; i ++ ) {
    
        const ni = i / rings;	 
        
        for ( let j = 0; j < i * parts; j ++ ) {
            
            const phi = Math.PI * 2 * j / ( i * parts );
            
            u = 0.5 * ( 1 + ni * Math.cos( phi ) );
            v = 1 - 0.5 * ( 1 + ni * Math.sin( phi ) );
            
            uvIdx  = ( rvSum + j ) * 2;
            
            uvs[ uvIdx     ] = u;
            uvs[ uvIdx + 1 ] = v;
            
        }
        
        rvSum += i * parts;
        
    }
    
    g.setVertices = ( t ) => {
    
        let x, y, z, posidx;
        
        vertices[ 0 ] = 0;
        vertices[ 1 ] = 0;
        vertices[ 2 ] = hf( 0, t );        

        rvSum = 1; // without center
        
        for ( let i = 0; i <= rings; i ++ ) {
        
            const ni = i / rings;	 
            
            for ( let j = 0; j < i * parts; j ++ ) {
                
                const phi = Math.PI * 2 * j / ( i * parts );
                
                x =  rf( r, phi, t ) * Math.cos( phi ) * ni;
                y = -rf( r, phi, t ) * Math.sin( phi ) * ni;
                z =  hf( ni, t );
                    
                posIdx = ( rvSum + j ) * 3;
                
                vertices[ posIdx     ] = x;
                vertices[ posIdx + 1 ] = y;
                vertices[ posIdx + 2 ] = z;
                
            }
            
            rvSum += i * parts;
            
        }
        
        g.computeVertexNormals( ) ;
        g.attributes.position.needsUpdate = true;
        
    }
    
    g.setVertices( );
    
}
5 Likes

In the meantime I have created a variant with shaders. CircleDynamicallyFormableShader

I have added stats. The shader variant is more performant.

However, the MeshPhongMaterial is not yet properly supported, since the calculation of the normals in the shader is still missing.

For this I first looked at the sources
Tutorial 8 : Basic shading

and to three.js
Calculating vertex normals after displacement in the vertex shader
Before I try to implement this, I’m interested to know if there are any new findings on this since March of this year.
:thinking:


<!DOCTYPE html>
<!-- https://discourse.threejs.org/t/a-dynamically-deformable-circle/33113 -->
<head>
  <title> CircleDynamicallyFormableShader </title>
  <meta charset="utf-8" />
  <style>
    body {
        overflow:hidden;
        margin: 0;
    }
  </style>
</head>
<body> </body>

<script type="module">

// @author hofk

import * as THREE from '../jsm/three.module.135.js';
import {OrbitControls} from '../jsm/OrbitControls.135.js';
import Stats from '../jsm/stats.module.135.js';

const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera( 55, window.innerWidth / window.innerHeight, 0.01, 10000 );
camera.position.set( 0.4, 0.3, 3.1 );
const renderer = new THREE.WebGLRenderer( { antialias: true } );
renderer.setSize( window.innerWidth, window.innerHeight );
renderer.setClearColor( 0xdedede );
document.body.appendChild( renderer.domElement );

const controls = new OrbitControls( camera, renderer.domElement );
const stats = new Stats( );
document.body.appendChild( stats.dom );

const light = new THREE.PointLight( );
light.position.set( -1, 2, 4);
scene.add( light );;

// inputs
const geometry = new THREE.BufferGeometry( );
const radius = 0.96;
const rings = 144;
const parts = 6;

CircleCustomShader( geometry, radius, rings, parts );

const uniforms = { u_time: { value: 0.0 } }

const sPart = shader => {  
    shader.uniforms.u_time = uniforms.u_time;
    shader.uniforms.u_radius = uniforms.u_radius;
    shader.vertexShader = `
      #define PI 3.141592653589793
      uniform float u_time;
      ${shader.vertexShader}
    `.replace(
      `#include <begin_vertex>`,
      `#include <begin_vertex>
        vec3 p = transformed;
        float len = length( p.xy );
        float phi = atan( p.x, p.y );
        p.xy +=  p.xy * 0.03 * len * sin( 18.0 * phi ) * ( 1.0 + cos( u_time ) );
        p.z = 0.04 * ( 1.0 + sin( PI * 12.0 * len ) ) * cos( u_time * 0.3 );
        transformed = p;
      `
    );
  }
 
const loader = new THREE.TextureLoader( );
const tex_1 = loader.load( 'uv_grid_opengl.jpg' ); 
const tex_2 = loader.load( 'rose.png' );   


const material_0 = new THREE.MeshPhongMaterial( { onBeforeCompile: sPart, color: 0xff00ff, side: THREE.DoubleSide, wireframe: false } );
const material_1 = new THREE.MeshBasicMaterial( { onBeforeCompile: sPart, map: tex_1, side: THREE.DoubleSide, wireframe: true} );
const material_2 = new THREE.MeshBasicMaterial( { onBeforeCompile: sPart, map: tex_2, side: THREE.DoubleSide, wireframe: false } );

const circle_0 = new THREE.Mesh( geometry, material_0 );
circle_0.position.x = -2;
scene.add( circle_0 );

const circle_1 = new THREE.Mesh( geometry, material_1);
scene.add( circle_1 );

const circle_2 = new THREE.Mesh( geometry, material_2 );
circle_2.position.x = 2;
scene.add( circle_2 );


animate( );

function animate() {

    requestAnimationFrame( animate );
    uniforms.u_time.value += 0.03;
    renderer.render( scene, camera );
    stats.update( );
}

function CircleCustomShader( g, r, rings, parts ) {
    
    const vertexCount = 1 + parts / 2 * rings * ( rings + 1 ) ;
    
    let idxCount = 0;
    let faceCount = parts * rings * rings;
    
    const faceIndices = new Uint32Array( faceCount * 3 );
    const vertices = new Float32Array( vertexCount * 3 );
    const uvs = new Float32Array( vertexCount * 2 );
       
    g.setIndex( new THREE.BufferAttribute( faceIndices, 1 ) );
    g.setAttribute( 'position', new THREE.BufferAttribute( vertices, 3 ) );
    g.setAttribute( 'uv', new THREE.BufferAttribute( uvs, 2 ) );
    
    const setFace =  ( ) => {
        
        faceIndices[ idxCount     ] = a;
        faceIndices[ idxCount + 1 ] = b;
        faceIndices[ idxCount + 2 ] = c;
        
        idxCount += 3;
        
    }
    
    let posIdx, uvIdx;
    
    let a = 0; // vertex 0: center
    let b = 1;
    let c = 2;
    
    for ( let j = 0; j < parts; j ++ ) { // around center
        
        setFace( );	
        b ++;
        if ( b < parts ) { c ++; } else { c = 1; }
        
    }
    
    let rvSum = 1; // only vertex 0
    
    for ( let i = 1; i < rings; i ++ ) {
        
        for ( let q = 0; q < parts; q ++ ) {
            
            for ( let j = 0; j < i + 1 ; j ++ ) {  
                
                if ( j === 0 ) {
                    
                    //  first face in part 
                    
                    a = rvSum;
                    b = a + parts * i + q;
                    c = b + 1;
                    
                    setFace();
                    
                } else {
                     
                    //  two faces / vertex
                    
                    a = j + rvSum; 
                    b = a - 1; 
                    c = a + parts * i + q;
                    if ( q === ( parts - 1 ) && j === i ) a = a - parts * i; // connect to first vertex of circle
                    
                    setFace();
                    
                    // a  from first face 
                    b = c; // from first face
                    c = b + 1;
                    
                    if ( q === ( parts - 1 ) && j === i ) c = c - parts * ( i + 1 ); // connect to first vertex of next circle
                    
                    setFace();
                    
                }
                
            }
            
            rvSum += i;
            
        }
        
    }   
    
    uvs[ 0 ] = 0.5;
    uvs[ 1 ] = 0.5;
    
    let u, v;
    
    rvSum = 1;  // without center
    
    for ( let i = 0; i <= rings; i ++ ) {
        
        const ni = i / rings;
        
        for ( let j = 0; j < i * parts; j ++ ) {
            
            const phi = Math.PI * 2 * j / ( i * parts );
            
            u = 0.5 * ( 1 + ni * Math.cos( phi ) );
            v = 1 - 0.5 * ( 1 + ni * Math.sin( phi ) );
            
            uvIdx  = ( rvSum + j ) * 2;
            
            uvs[ uvIdx     ] = u;
            uvs[ uvIdx + 1 ] = v;
            
        }
        
        rvSum += i * parts;
        
    }
    
    g.setVertices = ( t ) => {
    
        let x, y, z, posidx;
        
        vertices[ 0 ] = 0;
        vertices[ 1 ] = 0;
        vertices[ 2 ] = 0;
        
        rvSum = 1; // without center
        
        for ( let i = 0; i <= rings; i ++ ) {
        
            const ni = i / rings;
            
            for ( let j = 0; j < i * parts; j ++ ) {
                
                const phi = Math.PI * 2 * j / ( i * parts );
                
                x =  r * Math.cos( phi ) * ni;
                y = -r * Math.sin( phi ) * ni;
                z =  0; //Math.sin( Math.PI * 2 * ni );
                    
                posIdx = ( rvSum + j ) * 3;
                
                vertices[ posIdx     ] = x;
                vertices[ posIdx + 1 ] = y;
                vertices[ posIdx + 2 ] = z;
                
            }
            
            rvSum += i * parts;
            
        }
        
        g.computeVertexNormals( );
        
    }
    
    g.setVertices( );

}

</script>
</html>
2 Likes

Tried to compute normals in vertex shader: Edit fiddle - JSFiddle - Code Playground

Shader modification:

const sPart = shader => {  
  shader.uniforms.u_time = uniforms.u_time;
  shader.uniforms.u_radius = uniforms.u_radius;
  shader.vertexShader = `
  #define PI 3.141592653589793
  uniform float u_time;

  vec3 getPoint(vec3 p){
    float len = length( p.xy );
    float phi = atan( p.x, p.y );
    p.xy +=  p.xy * 0.03 * len * sin( 18.0 * phi ) * ( 1.0 + cos( u_time ) );
    p.z = 0.04 * ( 1.0 + sin( PI * 12.0 * len ) ) * cos( u_time * 0.3 );
    return p;
  }

	${shader.vertexShader}
`.replace(
    `#include <defaultnormal_vertex>`,
    `
    vec2 e = vec2(0.001, 0.);

    vec3 p0 = getPoint(position + e.yyy);
    vec3 p1 = getPoint(position + e.xyy);
    vec3 p2 = getPoint(position + e.yxy);

    vec3 t1 = p1 - p0;
    vec3 t2 = p2 - p0;

    objectNormal = cross(t1, t2);

    #include <defaultnormal_vertex>`
  )
    .replace(
    `#include <begin_vertex>`,
    `#include <begin_vertex>

    transformed = p0;
    `
  );
  //console.log(shader.vertexShader);
}
5 Likes

… is a strong understatement.

The solution looks clear to me and is obviously perfect. I especially like the way of determining p0,p1,p2.

I myself have only managed to apply the rather complicated codepen ( https://codepen.io/marco_fugaro/pen/xxZWPWJ from Calculating vertex normals after displacement in the vertex shader ) for my geometry in the meantime.

But I could not get the essential out of the mixture of controls-state
const controls = initControls({ ...
and
vertexShader: monkeyPatch(THREE.ShaderChunk.meshphysical_vert, {...
pull out.

In the solution I see that <defaultnormal_vertex> is the key. But how to do it, I certainly would not have thought of that. The extensive shader parts in three.module.js I do not see through.

Although I had tried to do something with
var defaultnormal_vertex = "vec3 transformedNormal = objectNormal; ...
but without success.

Can this way of determining vertex normals be directly integrated into three.js? Or are there reasons why this is not useful? :thinking:

I have incorporated the solution @prisoner849 in the example CircleDynamicallyFormableShader
and uploaded the update.

After the transfer into the shader part I got an incorrect display. A scattering error as I often do, but was not to be found. Later I noticed that prisoner849 changed the function CircleCustomShader. This for a good reason!

I copied the structure from one of my examples to spheres. There you need different orientations. I got exactly the wrong one and didn’t notice it because I always used
side: THREE.DoubleSide .

Actually I should learn from my mistakes, the same thing happened to me just a few months ago. :frowning_face:

const setFace =  ( ) => {
    
    faceIndices[ idxCount     ] = a;
    //faceIndices[ idxCount + 1 ] = b;
    //faceIndices[ idxCount + 2 ] = c;
    faceIndices[ idxCount + 1 ] = c; // swapped here c and b
    faceIndices[ idxCount + 2 ] = b; // otherwise, the geometry is backsided
    idxCount += 3;
    
}

I have also included the modified version in the zip file 2021 from
* discourse.threejs.hofk.de. So download it again if needed.

1 Like

Advanced application

The method can be used to achieve performant dynamic base geometries very easily.


const geometry = new THREE.CylinderBufferGeometry( 0.5, 0.5, 1, 360, 10, false );
or
const geometry = new THREE.BoxBufferGeometry( 1.0, 1.0, 1.0, 100, 100, 100 );

  vec3 getPoint( vec3 p ) {
    float r = length( p.xzy );
    float phi = atan( p.x, p.z );
    p.xz +=  p.xz * 0.2 * r * sin( 3.0 * phi ) * ( 1.0 + cos( u_time ) ); //   4.0 * phi box
    return p;
  }


const geometry = new THREE.PlaneBufferGeometry( 1.0, 1.0, 100, 100 );

 vec3 getPoint(vec3 p){
    float lenx = length( p.x);
    float leny = length( p.y);
    p.z = 0.04 * ( 1.0 + sin( PI * 24.0 * lenx * leny  ) ) * cos( u_time * 0.3 );
    return p;
  }


const geometry = new THREE.PlaneBufferGeometry( 1.0, 1.0, 100, 100 );

  float f( float d ) {       
        return 0.5 * ( 1.0 + sin( d ) ) * sqrt( d );           
  }
  vec3 getPoint( vec3 p ) {   
    float lenx = length( p.x );
    float leny = length( p.y );  
    p.z = ( lenx - leny ) * ( leny - lenx ) * f( lenx ) * f( leny ) * 6.0 * sin( u_time );
    // p.z = ( lenx + leny ) * ( leny + lenx ) * f( lenx ) * f( leny ) * 0.6 * sin( u_time ); // try out
    return p;
  }


const geometry = new THREE.TorusGeometry( 1, 0.5, 64, 100 );

  vec3 getPoint( vec3 p ) { 
    p.z =  abs( p.z ) > 0.3 ? sign( p.z ) * 0.3 : p.z; // symmetrical
    return p;  
  }

2022-01-09 20.26.01
see also Best way to flatten the top and bottom of a torus geometry - #7 by hofk


const geometry = new THREE.BoxGeometry( 1, 1, 1, 360, 1, 1 );

  vec3 getPoint( vec3 p ) {
   // @author prisoner89
    float r = 2.0;
    float R = 3.0;
    float waves = 5.0;
    float angle = p.x * PI * 2.0;
    float radius = p.z > 0.0 ? R : r;
    float y = p.y < 0.5 ? p.y : p.y + sin( p.x * PI * 2.0 * waves ) * 0.25;
    p = vec3( cos( angle ) * radius,  y, -sin( angle ) * radius );
    return p;  
  }

2022-01-09 20.53.38

see also How to create sine-wave groove in ring geometry with extrudegeometry? - #6 by prisoner849

1 Like