lass-clock

   

For this project, I wanted to make a tidal clock using 3js and shaders. The water rises and cycles through high and low tide twice every ~25 hours. Originally, my design involved a floating island with some water pooled in the middle, but as I worked on the project it I realized that floating islands didn't really make sense and instead used the shape of a tidal pool.

One feature of the clock is that the sky changes during day, night, and sunset. I chose to have this feature because since my shapes were low poly, I wanted to be able to have a wider variety of color palettes.    

       

One of the things that I would have liked to include in this project is using location services and to find the actual tide for the user's location. Right now, I'm basing the tide off of the most recent high tide on Virginia Beach, which will work temporarily but probably require calibration in the future since the length of tidal days is approximate. The same thing goes for sunrise and sunset times, since right now they are shown at a fixed time every day.

<script id="waterVertex" type="x-shader/x-vertex">
    uniform float u_time; 
    uniform float u_height; 
    varying vec3 v_position; 
    varying vec3 v_normal; 
 
    float random(vec2 co){
        return fract(sin(dot(co.xy ,vec2(12.9898,78.233))) * 43758.5453);
    }
 
    void main() {
        vec3 newPosition = position;    
        newPosition.z = cos(u_time / 3.0) / 10.0; 
        newPosition.z += u_height + 0.1; 
        newPosition.z += random(position.xy) / 15.0; 
        v_position = newPosition; 
        gl_Position = projectionMatrix * modelViewMatrix * vec4(newPosition, 1.0);
    }
</script>
<script id="waterFragment" type="x-shader/x-fragment">
    varying vec3 v_position; 
    uniform float u_radius; 
    uniform vec3 u_camera; 
    uniform float u_hour; 
 
    void main() {
        float fog = distance(u_camera, v_position) / 20.0; 
        float day = clamp(6.0 - abs(12.0 - u_hour), 0.0, 1.0); 
        vec3 darkBlue = vec3(fog / 4.0, 0.25, 0.5); 
        vec3 lightBlue = vec3(fog, 0.85, 0.8); 
        gl_FragColor =  vec4(day * lightBlue + (1.0 - day) * darkBlue, 0.8 - fog ); 
    }
</script>
<script id="sandVertex" type="x-shader/x-vertex">
    varying vec2 vUV;
    varying vec3 v_normal; 
    varying vec3 v_position; 
 
    void main() {  
        vUV = uv;
        vec4 pos = vec4(position, 1.0);
        gl_Position = projectionMatrix * modelViewMatrix * pos;
        v_normal = normalize(normal);
        v_position = position; 
    }
</script>
 
<script id="sandFragment" type="x-shader/x-fragment">
    varying vec3 v_normal; 
    varying vec3 v_position; 
    uniform vec3 u_camera; 
 
    void main() {
        vec3 lightSource = normalize(vec3(0.0, 1.0, 3.0)); 
        float dprod = max(dot(v_normal, lightSource), 0.0); 
        vec3 highlightColor = vec3( 0.7, 0.7, 0.2);
        vec3 shadowColor = vec3(0.3, 0.3, 0.6); 
        float fog = pow(distance(vec3(0.0), v_position) , 2.0) / 30.0; 
        gl_FragColor = vec4( shadowColor + highlightColor  * dprod * 0.4, 0.9 - fog);
    }
</script>
 
<script id="skyVertex" type="x-shader/x-fragment">  
    varying vec2 vUV;
    varying float v_z; 
 
    void main() {  
        vUV = uv;
        vec4 pos = vec4(position, 1.0);
        gl_Position = projectionMatrix * modelViewMatrix * pos;
        v_z = normalize(position).z; 
    }
</script>
 
<script id="skyFragment" type="x-shader/x-fragment">  
    varying float v_z; 
    uniform float u_hour; 
 
    void main() {  
        vec3 dayColor = vec3(0.4 - v_z, 0.7 - v_z, 0.80);
        vec3 sunsetColor = vec3(0.5 + v_z , 0.2 + v_z, 0.3);
        vec3 nightColor = vec3(0.05 - v_z / 10.0, 0.05 - v_z / 5.0, 0.20);
 
        vec3 dtn = vec3(0.0);
        dtn.x = clamp(6.0 - abs(12.0 - u_hour), 0.0, 1.0); 
        dtn.z = 1.0 - dtn.x; 
        dtn.y = (clamp(sin(u_hour * 3.14 / 12.0), 0.5, 1.0) - 0.5) * 2.0; 
        dtn = normalize(dtn); 
 
        gl_FragColor = vec4(dtn.x * dayColor + dtn.y * sunsetColor + dtn.z * nightColor, 1.0); 
    }
</script>  
 
 
<script>
 
    //https://thebookofshaders.com/04/
    var container;
    var camera, scene, renderer, controls;
    var waterUniforms, skyUniforms; 
    var water, tidehand, sand, clockface; 
    var angle, date, height; 
    var pastHighTide = new Date("September 20, 2018 5:12:00")
 
    init();
    animate();
 
    function init() {
        container = document.getElementById( "container" );
 
        camera = new THREE.PerspectiveCamera( 75, window.innerWidth/window.innerHeight, 0.1, 1000 );
        camera.position.z = 6;
        controls = new THREE.OrbitControls( camera );
        controls.maxDistance = 7; 
        controls.minDistance = 1; 
        controls.enableDamping = true; 
        controls.dampingFactor = 0.2;
        controls.maxAzimuthAngle = Math.PI / 4; 
        controls.minAzimuthAngle = Math.PI / -4; 
        controls.minPolarAngle = Math.PI / 4; 
        controls.maxPolarAngle = Math.PI * 3 / 4; 
 
        scene = new THREE.Scene();
 
        date = new Date(); 
        height = Math.cos(-1 * angle) / 7.0; 
 
        var geometry = new THREE.PlaneGeometry(50, 50, 100, 100); 
 
 
        waterUniforms = {
            u_time: { type: "f", value: 1.0 },
            u_height: { type: "f", value: height},
            u_camera: {type:"v3", value: camera.position}, 
            u_hour: {type: "f", value: date.getHours() + date.getMinutes() / 60}
        };
 
        var material = new THREE.ShaderMaterial( {
            uniforms: waterUniforms,
            vertexShader: document.getElementById("waterVertex").textContent,
            fragmentShader: document.getElementById("waterFragment").textContent, 
            transparent: true, 
            depthWrite: false, 
            side: THREE.DoubleSide
 
        } );
        water = new THREE.Mesh( geometry, material );
        scene.add( water );
 
        //sand geometry made in blender
        var loader = new THREE.JSONLoader();
        loader.load("tallsand.json", 
            function(geometry, materials){
                var sandUniforms = ({
                    u_camera: {type:"v3", value: camera.position}, 
                    u_hour: {type: "f", value: date.getHours() + date.getMinutes() / 60}
                }); 
                material = new THREE.ShaderMaterial( {
                    uniforms: sandUniforms, 
                    vertexShader: document.getElementById("sandVertex").textContent,
                    fragmentShader: document.getElementById("sandFragment").textContent, 
                    transparent: true, 
                } );
                var sandy = new THREE.Mesh(geometry, material); 
                sandy.rotation.x += Math.PI / 2; 
                sandy.scale.set(2, 2, 2); 
                sandy.position.z = -0.001; 
                scene.add(sandy); 
            }, 
            function(xhr){console.log("loaded json")},
            function(err){console.log("error loading json")}
        );
 
        geometry = new THREE.PlaneGeometry( .1, 2);
        material = new THREE.MeshBasicMaterial( { color: 0xffffff, side: THREE.DoubleSide, transparent:true} );
        stick = new THREE.Mesh( geometry, material );
        stick.position.y += .9; 
        tidehand = new THREE.Group(); 
        tidehand.add(stick); 
        tidehand.rotation.z = angle; 
        tidehand.position.z += 1.01; 
        scene.add( tidehand );
 
        var texture = new THREE.TextureLoader().load("clockface.png"); 
        geometry = new THREE.PlaneGeometry(6, 6); 
        material = new THREE.MeshBasicMaterial({map:texture, transparent: true, depthWrite: false}); 
        clockface = new THREE.Mesh(geometry, material); 
        clockface.position.z += 1;
        scene.add(clockface); 
 
        //skydome code from Ian Webster http://www.ianww.com/blog/2014/02/17/making-a-skydome-in-three-dot-js/
        //we are inside of a sphere! 
        geometry = new THREE.SphereGeometry(300, 60, 40);  
        skyUniforms = {
            u_hour: {type: "f", value: date.getHours() + date.getMinutes() / 60}
        }
        material = new THREE.ShaderMaterial( {  
            uniforms: skyUniforms,
            vertexShader:   document.getElementById("skyVertex").textContent,
            fragmentShader: document.getElementById("skyFragment").textContent,
            side: THREE.DoubleSide
        });
        skyBox = new THREE.Mesh(geometry, material);  
        skyBox.eulerOrder = "XZY";  
        skyBox.renderDepth = 1000.0;  
        scene.add(skyBox);  
 
        renderer = new THREE.WebGLRenderer();
        renderer.setPixelRatio( window.devicePixelRatio );
        container.appendChild( renderer.domElement );
        onWindowResize();
        window.addEventListener( "resize", onWindowResize, false );
    }
 
    function onWindowResize(event) {
        renderer.setSize( window.innerWidth, window.innerHeight );
    }
 
    function animate() {
        updateTide(); 
        waterUniforms.u_time.value += 0.05;
 
        controls.update(); 
        requestAnimationFrame( animate );
        render();
    }
 
    function updateTide() {
        date = new Date(); 
        //date.setMinutes(date.getMinutes() + 1); //artificial fast forward
        document.getElementById("info").innerHTML = date.toLocaleString(); 
        var diff = (date - pastHighTide) / 60000; 
        diff = diff % 745; 
        angle = -1 * Math.PI * 2 * diff / 745;
        height = Math.cos(-1 * angle) / 7.0; 
        waterUniforms.u_height.value = height; 
        skyUniforms.u_hour.value = date.getHours() + date.getMinutes() / 60; 
        waterUniforms.u_hour.value = date.getHours() + date.getMinutes() / 60; 
 
        tidehand.rotation.z = angle; 
    }
 
    function render() {
        renderer.render( scene, camera );
    }
 
    document.addEventListener("keydown", function(e){
        switch(e.keyCode){
            case 32: 
                controls.reset(); 
                break; 
        } 
    });
</script>