Morph box sphere geometry (+shader)

From a question Morphing between two geometries I made a dynamic geometry.

A box is morphed to a sphere and back by any function.

2021-11-22 21.23.07

2021-11-22 21.23.27

2021-11-22 21.22.57

Try it out MorphBoxSphere


// g  BufferGeometry, r radius, cs count of segments per side, v  norph function, result 0 .. 1
 
function CubeSphereGeometry( g, r, cs, v ) {
     
    // const sd = r * 1 / Math.sqrt( 2 ); // 1/2 square diagonal
    const ss = r * 1 / Math.sqrt( 3 ); // 1/2 square side
    
    const css = cs + 1;
    const hvc = css + cs ;   // height vertex count
    const vertexCount = css * css + cs * cs;
    const faceCount = cs * cs * 4;
    
    let a0, b0, c0, a1, b1, c1, le;
    
    const indices = new Uint32Array( faceCount * 3 * 6 );
    g.positions = new Float32Array( vertexCount * 3 * 6 );   
    uvs = new Float32Array( vertexCount * 2 * 6 ); // uvs to positions
    
    g.setIndex( new THREE.BufferAttribute( indices, 1 ) );
    g.setAttribute( 'position', new THREE.BufferAttribute( g.positions, 3 ).setUsage( THREE.DynamicDrawUsage) );
    g.setAttribute( 'uv', new THREE.BufferAttribute( uvs, 2 ) );
    
    g.cube = new Float32Array( vertexCount * 3 * 6 ); // basis positions plane,cube
    g.diff = new Float32Array( vertexCount * 3 * 6 ); // difference positions cube sphere
    
    let idx = 0;
    let offs = 0;
    
    makeIndices( );    

    idx = 0; // reset
    
               // ia, ib, ic, sign 
    makePositions( 0,  1,  2,  1 );
    makePositions( 0,  1,  2, -1 );
    makePositions( 1,  2,  0,  1 );
    makePositions( 1,  2,  0, -1 );
    makePositions( 2,  0,  1,  1 );
    makePositions( 2,  0,  1, -1 );
    
    idx = 0; // reset
    
         // ia, ib, o1, o2      ia, ib: index    o1, o2: orientation
    makeUVs( 0,  1,  1,  1 );
    makeUVs( 0,  1, -1,  1 );
    makeUVs( 1,  0,  1, -1 );
    makeUVs( 1,  0,  1,  1 );
    makeUVs( 1,  0,  1, -1 );
    makeUVs( 1,  0,  1,  1 );
    
    function makeIndices( ) {
    
        let materialIndex = 0;
        let startIdx = 0;
        let sign = 1;
        
        for ( let k = 0; k < 6; k ++ ) {
        
            for ( let j = 0; j < cs; j ++ ) {
            
                for ( let i = 0; i < cs; i ++ ) {
                
                    // 4 faces / segment
                    a  = offs + hvc * j + css + i;
                    
                    b1 = offs + hvc * j + i;                // bottom
                    c1 = offs + hvc * ( j + 1 ) + i;
                    
                    b2 = offs + hvc * j + 1 + i;            // left
                    c2 = offs + hvc * j + i;
                    
                    b3 = offs + hvc * ( j + 1 ) + i;        // right
                    c3 = offs + hvc * ( j + 1 ) + 1 + i;
                    
                    b4 = offs + hvc * ( j + 1 ) + 1 + i;    // top
                    c4 = offs + hvc * j + 1 + i;
                    
                    if ( sign === 1 ) {
                    
                        indices[ idx      ] = a;    // bottom
                        indices[ idx +  1 ] = b1;
                        indices[ idx +  2 ] = c1; 
                        
                        indices[ idx +  3 ] = a;    // left
                        indices[ idx +  4 ] = b2,
                        indices[ idx +  5 ] = c2; 
                        
                        indices[ idx +  6 ] = a;    // right
                        indices[ idx +  7 ] = b3;
                        indices[ idx +  8 ] = c3; 
                        
                        indices[ idx +  9 ] = a;    // top
                        indices[ idx + 10 ] = b4,
                        indices[ idx + 11 ] = c4
                        
                    }
                    
                    if ( sign === -1 ) {
                    
                        indices[ idx      ] = a;    // bottom
                        indices[ idx +  1 ] = c1;
                        indices[ idx +  2 ] = b1; 
                        
                        indices[ idx +  3 ] = a;    // left
                        indices[ idx +  4 ] = c2,
                        indices[ idx +  5 ] = b2; 
                        
                        indices[ idx +  6 ] = a;    // right
                        indices[ idx +  7 ] = c3;
                        indices[ idx +  8 ] = b3; 
                        
                        indices[ idx +  9 ] = a;    // top
                        indices[ idx + 10 ] = c4,
                        indices[ idx + 11 ] = b4
                        
                    }                    
                    
                    idx += 12;
                    
                }
            
            }
            
            g.addGroup ( startIdx,  idx - startIdx, materialIndex );
            materialIndex ++;
            startIdx = idx;
            
            offs += hvc * cs + css; // + vertex count one side
            
            sign = -sign;
            
        }
        
    }
    
    function makePositions( ia, ib, ic, sign ) {
        
        for ( let j = 0; j < css; j ++ ) {
        
            for ( let i = 0; i < css; i ++ ) {
                
                a0 = -ss + 2 * ss * j / cs;
                b0 = -ss + 2 * ss * i / cs;
                c0 = sign * ss;
                
                g.cube[ idx + ia ] = a0;
                g.cube[ idx + ib ] = b0;
                g.cube[ idx + ic ] = c0;
                
                le = Math.sqrt( a0 * a0 + b0 * b0 + c0 * c0 );       
                
                a1 = r * a0 / le;
                b1 = r * b0 / le;
                c1 = r * c0 / le;
                
                g.diff[ idx + ia ] = a1 - a0;
                g.diff[ idx + ib ] = b1 - b0;
                g.diff[ idx + ic ] = c1 - c0;
                
                g.positions[ idx + ia ] = a0; 
                g.positions[ idx + ib ] = b0;
                g.positions[ idx + ic ] = c0;        
                
                idx += 3;
                
            }
            
            if( j < cs ) {
                
                for ( let i = 0; i < cs; i ++ ) {
                        
                    a0 = -ss + 2 * ss * ( j + 0.5 ) / cs;
                    b0 = -ss + 2 * ss * ( i + 0.5 ) / cs;
                    c0 = sign * ss;
                    
                    g.cube[ idx + ia ] = a0;
                    g.cube[ idx + ib ] = b0;
                    g.cube[ idx + ic ] = c0;
                    
                    le = Math.sqrt( a0 * a0 + b0 * b0 + c0 * c0 );
                    
                    a1 = r * a0 / le;
                    b1 = r * b0 / le;
                    c1 = r * c0 / le;
                    
                    g.diff[ idx + ia ] = a1 - a0;
                    g.diff[ idx + ib ] = b1 - b0;
                    g.diff[ idx + ic ] = c1 - c0;
                    
                    g.positions[ idx + ia ] = a0;
                    g.positions[ idx + ib ] = b0;
                    g.positions[ idx + ic ] = c0;
                    
                    idx += 3;
                    
                }
                
            }
            
        }
        
    } 
    
    function makeUVs( ia, ib, o1, o2 ) {
        
        for ( let j = 0; j < css; j ++ ) {
        
            for ( let i = 0; i < css; i ++ ) {
                
                uvs[ idx + ia ] = o1 === 1 ? j / cs : 1 - j / cs;
                uvs[ idx + ib ] = o2 === 1 ? i / cs : 1 - i / cs;    
                
                idx += 2;
                
            }
            
            if( j < cs ) {
                
                for ( let i = 0; i < cs; i ++ ) {
                        
                    uvs[ idx + ia ] = o1 === 1 ? ( j + 0.5 ) / cs : 1 - ( j + 0.5 ) / cs;
                    uvs[ idx + ib ] = o2 === 1 ? ( i + 0.5 ) / cs : 1 - ( i + 0.5 ) / cs;
                    
                    idx += 2;
                    
                }
                
            }
            
        } 
        
    }
    
    g.morph = function morph( t ) {
    
        idx = 0; // reset
                  // ia, ib, ic   ia, ib, ic : index  
        setPositions( 0,  1,  2 );
        setPositions( 0,  1,  2 );
        setPositions( 1,  2,  0 );
        setPositions( 1,  2,  0 );
        setPositions( 2,  0,  1 );
        setPositions( 2,  0,  1 );
        
        g.attributes.position.needsUpdate = true;// to change the positions of the vertices
        
        function setPositions( ia, ib, ic ) {
            
            for ( let j = 0; j < css; j ++ ) {
            
                for ( let i = 0; i < css; i ++ ) {
                    
                    g.positions[ idx + ia ] = g.cube[ idx + ia ] + v( t ) * g.diff[ idx + ia ]; 
                    g.positions[ idx + ib ] = g.cube[ idx + ib ] + v( t ) * g.diff[ idx + ib ];
                    g.positions[ idx + ic ] = g.cube[ idx + ic ] + v( t ) * g.diff[ idx + ic ];        
                
                    idx += 3;
                    
                }
            
                if( j < cs ) {
                    
                    for ( let i = 0; i < cs; i ++ ) {
                        
                        g.positions[ idx + ia ] = g.cube[ idx + ia ] + v( t ) * g.diff[ idx + ia ]; 
                        g.positions[ idx + ib ] = g.cube[ idx + ib ] + v( t ) * g.diff[ idx + ib ];
                        g.positions[ idx + ic ] = g.cube[ idx + ic ] + v( t ) * g.diff[ idx + ic ];        
                    
                        idx += 3;
                    
                    }
                
                }
                
            }
            
        }
        
    }
    
}
6 Likes

I like the grid :slight_smile: Built the same here: https://jsfiddle.net/prisoner849/05gvwt1j/

oh wow!! this is so awesome!! :raised_hands:t4:

Since I’ve recently started looking a bit more closely at shaders, I created this dynamic geometry in that way as well. I used THREE.BoxBufferGeometry to do this.

MorphBoxSphereShader

Essential is
const a = 2 / Math.sqrt( 3 ); // means r === 1, important for: normalize( transformed ) - transformed

transformed += vec3( 0.5 * ( 1.0 + sin( u_time ) ) * ( normalize( transformed ) - transformed ) );

Create any radii with mesh.scale.set( , , ); .

For a better comparison I changed the original version to modules and added stats.
With a large number of vertices, the shader version is significantly more performant.
However, a different grid is also used.



<!DOCTYPE html> 
<!-- https://discourse.threejs.org/t/morph-box-sphere-geometry/31986 -->
<head>
	<title> MorphBoxSphereShader </title>
	<meta charset="utf-8" />
	<style>
      body { 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( 60, window.innerWidth / window.innerHeight, 0.1, 100 );
camera.position.set( 0, 0, 10 );
const renderer = new THREE.WebGLRenderer( { antialias: true } );
renderer.setSize( window.innerWidth, window.innerHeight );
renderer.setClearColor( 0xdedede, 1 );	
const container = document.createElement( 'div' );
document.body.appendChild( container );
container.appendChild( renderer.domElement );

const controls = new  OrbitControls( camera, renderer.domElement );
const stats = new Stats( );
container.appendChild( stats.dom );
 
scene.add( new THREE.AxesHelper( 2 ) );
//scene.add( new THREE.GridHelper( 2, 20 ) );

const a = 2 / Math.sqrt( 3 ); // means r === 1, important for: normalize( transformed ) - transformed

const g = new THREE.BoxBufferGeometry( a, a, a, 36, 36, 36 );
 
const textureLoader = new THREE.TextureLoader( );

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

const uvMap = textureLoader.load("uv_grid_opengl.jpg");

const sPart = shader => {  
    shader.uniforms.u_time = uniforms.u_time;   
    shader.vertexShader = `
      uniform float u_time;      
      ${shader.vertexShader}
    `.replace(
      `#include <begin_vertex>`,
      `#include <begin_vertex>
       
       transformed += vec3( 0.5 * ( 1.0 + sin(  u_time  ) ) * ( normalize( transformed ) - transformed ) );
      `
    );
  }

const uvMaterial = new THREE.MeshBasicMaterial( { onBeforeCompile: sPart, map: uvMap,  wireframe: false } );
const uvMesh = new THREE.Mesh( g, uvMaterial );
uvMesh.position.x = 4;
uvMesh.scale.set( 2, 2, 2 );
scene.add( uvMesh );

const basicMaterial = new THREE.MeshBasicMaterial( { onBeforeCompile: sPart, color: 0xff00ff, wireframe: true, side: THREE.DoubleSide } );
const basicMesh = new THREE.Mesh( g, basicMaterial );
basicMesh.scale.set( 2, 2, 2 );
scene.add( basicMesh )

const diceMaps = [
    textureLoader.load( 'dice/6.png' ),
    textureLoader.load( 'dice/1.png' ),
    textureLoader.load( 'dice/5.png' ),
    textureLoader.load( 'dice/2.png' ),
    textureLoader.load( 'dice/4.png' ),
    textureLoader.load( 'dice/3.png' ),   
]

let diceMaterial = [];

for ( let i = 0; i < 6; i ++ ) {
    
  diceMaterial.push( new THREE.MeshBasicMaterial( { onBeforeCompile: sPart, map: diceMaps[ i ], wireframe: false } ) );
    
}
 
const diceMesh = new THREE.Mesh( g, diceMaterial );
diceMesh.position.x = -4;
diceMesh.scale.set( 2, 2, 2 );
scene.add( diceMesh );

animate();

function animate( ) {

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

</script>
</html>
1 Like

I used mixing between positions of box and computed positions of sphere: Edit fiddle - JSFiddle - Code Playground
So, basically, it’s just:

        vec3 boxPos = position;
        vec3 spherePos = normalize(position) * radius;
      	transformed = mix(boxPos, spherePos, aVal); // aVal is a uniform, in range 0..1

Interesting
let radius =... // sphere of the same volume

I had the goal of inflating the cube. Then the 8 corners stay in place.

Ah, then the radius is Math.sqrt(3) * size * 0.5, a half or the cube’s diagonal :thinking:

This is the formula.

Maybe you can just get this into the shader and then you can choose the radius as you like.

Pass the cube’s size in a uniform and then compute the radius in shaders:

uniform float cubeSize;
...
float radius = sqrt(3.) * cubeSize * 0.5; 
vec3 spherePos = normalize(position) * radius;

But, I think, it would be better to compute the radius once on js side and then pass it in a uniform.

Works quite simple, see MorphBoxSphereShaderRadius

const r = 1.5; // radius of the sphere
const a = r * 2 / Math.sqrt( 3 ); //  size of the box, corners on sphere

const g = new THREE.BoxBufferGeometry( a, a, a, 36, 36, 36 );

Shader code snippet

    transformed /= u_radius;       
    transformed += vec3( 0.5 * ( 1.0 + sin(  u_time  ) ) * ( normalize( transformed ) - transformed ) );
    transformed *= u_radius;


<!DOCTYPE html> 
<!-- https://discourse.threejs.org/t/morph-box-sphere-geometry/31986/10 -->
<head>
	<title> MorphBoxSphereShaderRadius </title>
	<meta charset="utf-8" />
	<style>
      body { 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( 60, window.innerWidth / window.innerHeight, 0.1, 100 );
camera.position.set( 0, 0, 7 );
const renderer = new THREE.WebGLRenderer( { antialias: true } );
renderer.setSize( window.innerWidth, window.innerHeight );
renderer.setClearColor( 0xdedede, 1 );	
const container = document.createElement( 'div' );
document.body.appendChild( container );
container.appendChild( renderer.domElement );

const controls = new  OrbitControls( camera, renderer.domElement );
const stats = new Stats( );
container.appendChild( stats.dom );
 
scene.add( new THREE.AxesHelper( 1.5 ) );

const r = 1.5; // radius of the sphere
const a = r * 2 / Math.sqrt( 3 ); //  size of the box, corners on sphere

const g = new THREE.BoxBufferGeometry( a, a, a, 36, 36, 36 );
 
const textureLoader = new THREE.TextureLoader( );

const uniforms = { u_time: { value: 0.0 }, u_radius: { value: r } }

const uvMap = textureLoader.load("uv_grid_opengl.jpg");

const sPart = shader => {  
    shader.uniforms.u_time = uniforms.u_time;
    shader.uniforms.u_radius = uniforms.u_radius;
    shader.vertexShader = `
      uniform float u_time;
      uniform float u_radius;
      ${shader.vertexShader}
    `.replace(
      `#include <begin_vertex>`,
      `#include <begin_vertex>
        transformed /= u_radius;       
        transformed += vec3( 0.5 * ( 1.0 + sin(  u_time  ) ) * ( normalize( transformed ) - transformed ) );
        transformed *= u_radius;
      `
    );
  }

const uvMaterial = new THREE.MeshBasicMaterial( { onBeforeCompile: sPart, map: uvMap, wireframe: false } );
const uvMesh = new THREE.Mesh( g, uvMaterial );
uvMesh.position.x = 3;
scene.add( uvMesh );

const basicMaterial = new THREE.MeshBasicMaterial( { onBeforeCompile: sPart, color: 0xff00ff, wireframe: true, side: THREE.DoubleSide } );
const basicMesh = new THREE.Mesh( g, basicMaterial );
scene.add( basicMesh )

const diceMaps = [
    textureLoader.load( 'dice/6.png' ),
    textureLoader.load( 'dice/1.png' ),
    textureLoader.load( 'dice/5.png' ),
    textureLoader.load( 'dice/2.png' ),
    textureLoader.load( 'dice/4.png' ),
    textureLoader.load( 'dice/3.png' )
]

let diceMaterial = [];

for ( let i = 0; i < 6; i ++ ) {
    
    diceMaterial.push( new THREE.MeshBasicMaterial( { onBeforeCompile: sPart, map: diceMaps[ i ], wireframe: false } ) );
    
}
 
const diceMesh = new THREE.Mesh( g, diceMaterial );
diceMesh.position.x = -3;
scene.add( diceMesh );

animate();

function animate( ) {

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

</script>
</html>
1 Like

Since I haven’t been working with shaders for very long, I only noticed now that you can optimize.

The onBeforeCompile parts are identical, you can bring them into a function. Code changed above.


const sPart = shader => {  
    shader.uniforms.u_time = uniforms.u_time;   
    shader.vertexShader = `
      uniform float u_time;      
      ${shader.vertexShader}
    `.replace(
      `#include <begin_vertex>`,
      `#include <begin_vertex>
       
       transformed += vec3( 0.5 * ( 1.0 + sin(  u_time  ) ) * ( normalize( transformed ) - transformed ) );
      `
    );
  }

and

const sPart = shader => {  
    shader.uniforms.u_time = uniforms.u_time;
    shader.uniforms.u_radius = uniforms.u_radius;
    shader.vertexShader = `
      uniform float u_time;
      uniform float u_radius;
      ${shader.vertexShader}
    `.replace(
      `#include <begin_vertex>`,
      `#include <begin_vertex>
        transformed /= u_radius;       
        transformed += vec3( 0.5 * ( 1.0 + sin(  u_time  ) ) * ( normalize( transformed ) - transformed ) );
        transformed *= u_radius;
      `
    );
  }