<div id='root' class='full-screen-3d-example'></div>

<script src='https://unpkg.com/three@0.146.0/build/three.min.js'></script>
<script src='https://unpkg.com/three@0.146.0/examples/js/controls/OrbitControls.js'></script>
* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

body {
    overflow: hidden;
    background: #eeeeee;
}

.full-screen-3d-example {
    opacity: 0;
    transition: opacity 1s ease-out;
}

.full-screen-3d-example.-loaded {
    opacity: 1;
}
const { THREE } = window;

const FLAGS = Object.freeze({
    ENABLE_ORBIT_CONTROLS: false,
});

const COLOR_PALETTE = Object.freeze({
    black: 0x010101,
    white: 0xeeeeee,
    wave: 0xe63946,
});

const GLSL_UTILS = Object.freeze({
    rand: `
        float crand(vec2 seed) {
            return fract(sin(dot(seed, vec2(12.9898,78.233))) * 43758.5453123);
        }
    `,
    noise: `
        float noise(vec2 position) {
            vec2 p = floor(position);
            float tl = crand(p);
            float tr = crand(p + vec2(1.0, 0.0));
            float bl = crand(p + vec2(0.0, 1.0));
            float br = crand(p + vec2(1.0, 1.0));
            vec2 v = smoothstep(0.0, 1.0, fract(position));

            return mix(tl, tr, v.x)
                + (bl - tl) * v.y * (1.0 - v.x)
                + (br - tr) * v.x * v.y
                - 0.5;
        }
    `,
});

class CustomMaterial extends THREE.MeshStandardMaterial {
    onBeforeCompile(shader) {
        // eslint-disable-next-line no-param-reassign
        shader.uniforms.uTime = { value: 0.0 };

        // eslint-disable-next-line no-param-reassign
        shader.vertexShader = shader.vertexShader.replace(
            '#include <uv_pars_vertex>',
            `varying vec2 vUv;
            uniform float uTime;`,
        );

        // eslint-disable-next-line no-param-reassign
        shader.vertexShader = shader.vertexShader.replace(
            '#include <uv_vertex>',
            'vUv = uv;',
        );

        // eslint-disable-next-line no-param-reassign
        shader.fragmentShader = shader.fragmentShader.replace(
            'varying vec3 vViewPosition;',
            `varying vec3 vViewPosition;
            varying vec2 vUv;
            uniform float uTime;`,
        );

        this.userData.shader = shader;
    }
}

class WavesMaterial extends CustomMaterial {
    constructor() {
        super({
            color: COLOR_PALETTE.wave,
            side: THREE.DoubleSide,
        });
    }

    onBeforeCompile(shader) {
        super.onBeforeCompile(shader);

        // eslint-disable-next-line no-param-reassign
        shader.vertexShader = `
            ${GLSL_UTILS.rand}
            ${GLSL_UTILS.noise}

            float getWaveZ(vec2 xy, float time) {
                float t = 0.2 * time;

                return 30.0 * sin(3.3 * uv.x)
                    + 5.0 * sin(0.1 * xy.y) * noise(0.2 * xy - 2.0 * t);
            }

            ${shader.vertexShader}
        `;

        // eslint-disable-next-line no-param-reassign
        shader.vertexShader = shader.vertexShader.replace(
            '#include <begin_vertex>',
            `
                #include <begin_vertex>

                transformed =
                    position
                    + vec3(0.0, 0.0, getWaveZ(position.xy, uTime));

                float d = 0.0001;

                vNormal = normalize(
                    cross(
                        vec3(  d, 0.0, getWaveZ(vec2(position.x + d, position.y), uTime))
                        -vec3(-d, 0.0, getWaveZ(vec2(position.x - d, position.y), uTime)),
                        vec3( 0.0,  d, getWaveZ(vec2(position.x, position.y + d), uTime))
                        -vec3(0.0, -d, getWaveZ(vec2(position.x, position.y - d), uTime))
                    )
                );
            `,
        );

        // eslint-disable-next-line no-param-reassign
        shader.fragmentShader = `
            ${GLSL_UTILS.rand}
            ${GLSL_UTILS.noise}
            ${shader.fragmentShader}
        `;

        // eslint-disable-next-line no-param-reassign
        shader.fragmentShader = shader.fragmentShader.replace(
            '#include <map_fragment>',
            `
                #include <map_fragment>

                float c = 0.5 * abs(noise(vec2(30.0 * vUv.x, 300.0 * vUv.y)));

                diffuseColor = diffuseColor - vec4(c, c, c + 0.05, 0.0);
            `,
        );

        this.userData.shader = shader;
    }
}

class MaterialsLibrary {
    static waves = new WavesMaterial();
}

class SandboxWorld extends THREE.Group {
    constructor() {
        super();

        this.#initObjects();
        this.#initLights();
    }

    #initObjects() {
        const geometry = new THREE.PlaneGeometry(80, 30, 200, 100);
        const material = MaterialsLibrary.waves;
        const wave1 = new THREE.Mesh(geometry, material);

        this.add(wave1);

        const wave2 = new THREE.Mesh(geometry, material);

        wave2.position.set(5, 0, 5);
        wave2.rotation.set(Math.PI * 0.8, 0.1, 0.1);

        this.add(wave2);

        const wave3 = new THREE.Mesh(geometry, material);

        wave3.position.set(10, 0, -2);
        wave3.rotation.set(Math.PI * 0.8, 0.3, 0.1);

        this.add(wave3);
    }

    #initLights() {
        const ambient = new THREE.AmbientLight(COLOR_PALETTE.white, 0.5);

        this.add(ambient);

        const directional = new THREE.DirectionalLight(COLOR_PALETTE.white);

        directional.position.set(1, 0.2, 1);

        this.add(directional);
    }
}

class FullScreen3DExample {
    static CSS_ROOT = 'full-screen-3d-example';
    static CSS_ROOT_LOADED_VARIANT = '-loaded';

    #root;
    #frameRequestId;
    #scene;
    #world;
    #camera;
    #controls;
    #renderer;

    constructor(root) {
        this.#root = root;
        this.#root.classList.add(FullScreen3DExample.CSS_ROOT);
        this.#initScene();
        this.#initObjects();
        this.#initCamera();
        this.#initRenderer();
        this.#initControls();
        this.#initEventListeners();
        this.#onWindowResize();
        this.#root.classList.add(FullScreen3DExample.CSS_ROOT_LOADED_VARIANT);
        this.#render();
    }

    #initScene() {
        this.#scene = new THREE.Scene();
    }

    #initObjects() {
        this.#world = new SandboxWorld();

        this.#scene.add(this.#world);
    }

    #initCamera() {
        const fov = 45;
        const aspect = window.innerWidth / window.innerHeight;
        const near = 1;
        const far = 1000;

        this.#camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
        this.#camera.position.set(3, -55, 0);
        this.#camera.lookAt(new THREE.Vector3(0.0, 0.0, 0.0));
    }

    #initRenderer() {
        const clearColor = COLOR_PALETTE.white;
        const clearColorAlpha = 1;

        this.#renderer = new THREE.WebGLRenderer({
            alpha: false,
            logarithmicDepthBuffer: true,
        });

        this.#renderer.setClearColor(clearColor, clearColorAlpha);
        this.#renderer.setPixelRatio(window.devicePixelRatio);

        if (FLAGS.ENABLE_SHADOWS) {
            this.#renderer.shadowMap.enabled = true;
            this.#renderer.shadowMap.type = THREE.PCFSoftShadowMap;
        }

        this.#root.appendChild(this.#renderer.domElement);
    }

    #initControls() {
        if (FLAGS.ENABLE_ORBIT_CONTROLS) {
            this.#controls = new THREE.OrbitControls(
                this.#camera,
                this.#renderer.domElement,
            );
        }
    }

    #initEventListeners() {
        window.addEventListener('resize', this.#onWindowResize.bind(this));
    }

    #onWindowResize() {
        const width = window.innerWidth;
        const height = window.innerHeight;

        this.#camera.aspect = width / height;
        this.#camera.updateProjectionMatrix();
        this.#renderer.setSize(width, height);
    }

    #updateEverything() {
        const t = performance.now() / 1000;

        this.#scene.traverse((child) => {
            if (child.isMesh) {
                const { shader } = child.material.userData;

                if (shader) {
                    shader.uniforms.uTime.value = t;
                }
            }
        });
    }

    #render() {
        this.#updateEverything();

        this.#renderer.render(this.#scene, this.#camera);
    }

    start() {
        this.#render();

        if (FLAGS.ENABLE_ORBIT_CONTROLS) {
            this.#controls.update();
        }

        this.#frameRequestId = requestAnimationFrame(this.start.bind(this));
    }

    stop() {
        cancelAnimationFrame(this.#frameRequestId);
    }
}

function main() {
    const root = document.getElementById('root');
    const example = new FullScreen3DExample(root);

    example.start();
}

document.addEventListener('DOMContentLoaded', main);
View Compiled

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.