body {
	background-color: #000;
	margin: 0px;
	overflow: hidden;
}
const renderer = new THREE.WebGLRenderer( {
	alpha: false,
  antialias: true
} );
renderer.setSize( window.innerWidth, window.innerHeight );
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
document.body.appendChild( renderer.domElement );

const scene = new THREE.Scene;

const camera = new THREE.PerspectiveCamera( 40, window.innerWidth / window.innerHeight, 0.1, 10000 );
new THREE.OrbitControls( camera, renderer.domElement );

camera.position.set( 9, 4, 0 );
camera.updateMatrixWorld( true );
camera.lookAt( new THREE.Vector3( 0, 2, 0 ) );
camera.updateMatrixWorld( true );

sky = new THREE.Sky();
sky.scale.setScalar( 450000 );
scene.add( sky );

var uniforms = sky.material.uniforms;

uniforms[ "turbidity" ].value = 10;
uniforms[ "rayleigh" ].value = 2;
uniforms[ "mieCoefficient" ].value = 0.005;
uniforms[ "mieDirectionalG" ].value = 0.8;
uniforms[ "sunPosition" ].value.set(400000, 400000, 400000);

const light = new THREE.DirectionalLight( 0xffffff, 1 );
light.position.set( 30, 30, 30 );
light.castShadow = true;
light.shadow.mapSize.width = 2048;
light.shadow.mapSize.height = 2048;
light.shadow.camera.top = 24;
light.shadow.camera.right = 24;
light.shadow.camera.bottom = -24;
light.shadow.camera.left = -24;
light.shadow.camera.far = 64;
scene.add( light );

scene.add( new THREE.HemisphereLight( 0xd7f6fa, 0x323436, 1 ) );

const floor = new THREE.Mesh( new THREE.PlaneBufferGeometry( 17, 17, 1, 1 ), new THREE.MeshStandardMaterial( {
  color: new THREE.Color( 'white' ),
  roughness: 1,
  metalness: 0,
  transparent: true,
  blending: THREE.MultiplyBlending
} ) );
floor.rotation.x = -90 * 0.01745327;
floor.receiveShadow = true;
scene.add( floor );
scene.background = new THREE.Color( 'white' );


const uTime = {
	shared: true,
	mixed: true,
	value: 0
};


// Creating 2 new materials, each based on THREE.MeshStandardMaterial adding new features

let testMaterial;

const bouncingMaterial = THREE.extendMaterial( THREE.MeshStandardMaterial, {

	class: THREE.CustomMaterial,  // In this case ShaderMaterial would be fine too, just for some features such as envMap this is required
  
	vertexHeader: 'uniform float offsetScale;',
	vertex: {
		transformEnd: 'transformed += normal * offsetScale;'
	},

	uniforms: {
    roughness: 0.75,
		offsetScale: {
			mixed: true,    // Uniform will be passed to a derivative material (MeshDepthMaterial below)
			linked: true,   // Similar as shared, but only for derivative materials, so wavingMaterial will have it's own, but share with it's shadow material
			value: 0
		}
	}

} );


const wavingMaterial = THREE.extendMaterial( bouncingMaterial, {

	vertexHeader: 'uniform float uTime;',
	vertex: {
		transformEnd: 'transformed.z += sin( position.y * 4.0 + uTime ) * 0.25;'
	},

	uniforms: {
		uTime
	}

} );


testMaterial = wavingMaterial;


const mesh = new THREE.Mesh( new THREE.TorusKnotBufferGeometry( .7, .1, 100, 16 ), testMaterial );
mesh.castShadow = true;
mesh.receiveShadow = true;
mesh.position.z = -3;
mesh.position.y = 2;

// Meshes are patched so you can store the material variant on the original material instead having to assign it to every new instance ( see next: clone )
testMaterial.customDepthMaterial = THREE.extendMaterial( THREE.MeshDepthMaterial, {

	template: testMaterial

} );



const clone = mesh.clone();
clone.position.z = 3;

// We want a individual offsetScale parameter for mesh2 one so we clone the material, the depth material automatically gets cloned and linked to the new material as well
const mesh2 = mesh.clone();
mesh2.material = mesh.material.clone();
mesh2.material.uniform( 'diffuse' ).value = new THREE.Color( 0xce2154 );

mesh2.position.x = 2;
mesh2.position.z = 0;

// Tip: if you want to avoid creating new materials just for individual uniforms you can also have a look at <link>
//      this let's you share materials and store the individual "parameters" for uniforms on each mesh instead.


scene.add( mesh, clone, mesh2 );


let t = 0.0;

function render() {

	t += 0.05;

	uTime.value = t;

	mesh.material.uniforms.offsetScale.value = Math.abs( Math.sin( t ) ) * .15;
	mesh2.material.uniforms.offsetScale.value = Math.abs( Math.sin( t * 2.0 ) ) * .15;

	requestAnimationFrame( render );

	renderer.render( scene, camera );

}


render();
View Compiled
Run Pen

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

  1. https://unpkg.com/three@0.125.0
  2. https://threejs.org/examples/js/controls/OrbitControls.js
  3. https://threejs.org/examples/js/controls/TransformControls.js
  4. https://mevedia.com/share/ExtendMaterial.js?vxx1
  5. https://mevedia.com/share/Sky.js