// --------------------- // | setup of three.js | // --------------------- // shaders for sphere outline // https://stemkoski.github.io/Three.js/Shader-Glow.html const vertexShaderGlow = ` uniform vec3 viewVector; uniform float c; uniform float p; varying float intensity; varying vec3 vColor; vec3 hsv2rgb(vec3 c) { vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0); vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www); return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y); } void main() { vec3 vNormal = normalize( normalMatrix * normal ); vec3 vNormel = normalize( normalMatrix * viewVector ); intensity = pow( c - dot(vNormal, vNormel), p ); gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 ); vColor = hsv2rgb(vec3(fract(gl_Position[2] / 270.0), 0.7, 0.7)); //vColor = vec3((sin(position[2] / 360.0) + 1.0) * 0.5, (sin(position[2] / 1000.0) + 1.0) * 0.5, (sin(position[2] / 70.0) + 1.0) * 0.5); //vColor = vec3(0.5, 0, 0.5); } `; const fragmentShaderGlow = ` varying vec3 vColor; varying float intensity; void main() { vec3 glow = vColor * intensity; gl_FragColor = vec4( glow, 1.0 ); } `; // shaders for tunnel border const fragmentShader = ` #include uniform vec3 iResolution; uniform float iTime; uniform sampler2D iChannel0; // By Daedelus: https://www.shadertoy.com/user/Daedelus // license: Creative Commons Attribution-NonCommercial-ShareAlike 3.0 Unported License. #define TIMESCALE 0.25 #define TILES 8 #define COLOR 0.7, 1.6, 2.8 varying vec2 vUv; varying float z; void mainImage( out vec4 fragColor, in vec2 fragCoord ) { vec2 uv = fragCoord.xy / iResolution.xy; uv.x *= iResolution.x / iResolution.y; vec4 noise = texture2D(iChannel0, floor(uv * float(TILES)) / float(TILES)); float p = 1.0 - mod(noise.r + noise.g + noise.b + (iTime + z) * float(TIMESCALE), 1.0); p = min(max(p * 3.0 - 1.8, 0.1), 2.0); vec2 r = mod(uv * float(TILES), 1.0); r = vec2(pow(r.x - 0.5, 2.0), pow(r.y - 0.5, 2.0)); p *= 1.0 - pow(min(1.0, 12.0 * dot(r, r)), 2.0); fragColor = vec4(COLOR, 1.0) * p; } void main() { mainImage(gl_FragColor, vUv * iResolution.xy); } `; const vertexShader = ` varying vec2 vUv; varying float z; void main() { vUv = uv; z = -position[1]; gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 ); } `; // size of the tunnel const tunnelRadius = 100; // z interval of new obstacles const spawnInterval = 40; // coordinate of newest obstacle let lastSpawned = 0; // gyroscope variables var gn; let headSet = false; let gammaReference = 0.0; let leftRightMove = 0.0; let upDownMove = 0.0; // current player speed const initialSpeed = 5.0; let speed = initialSpeed; // score points let score = 0; // amount of sphere objects removed let removed = 0; // player is actively racing let running = false; // obstacles const spheres = []; // wall segments const borders = []; let time = Date.now() / 1000.0; let averageFps = 60.0; function logger(text) { console.log(text); } function init_gn() { const args = { logger: logger }; gn = new GyroNorm(); gn.init(args).then(function() { const isAvailable = gn.isAvailable(); if (!isAvailable.deviceOrientationAvailable) { console.log({ message: 'Device orientation is not available.' }); } if (!isAvailable.accelerationAvailable) { console.log({ message: 'Device acceleration is not available.' }); } if (!isAvailable.accelerationIncludingGravityAvailable) { console.log({ message: 'Device acceleration incl. gravity is not available.' }); } if (!isAvailable.rotationRateAvailable) { console.log({ message: 'Device rotation rate is not available.' }); } start_gn(); }).catch(function(e) { console.error(e); document.getElementById("start").disabled = false; }); document.addEventListener('keydown', e => { if (e.key === "ArrowLeft") { leftRightMove = -10.0; } else if (e.key === "ArrowRight") { leftRightMove = 10.0; } else if (e.key === "ArrowDown") { upDownMove = -10.0; } else if (e.key === "ArrowUp") { upDownMove = 10.0; } }); document.addEventListener('keyup', e => { if (e.key === "ArrowLeft") { leftRightMove = 0; } else if (e.key === "ArrowRight") { leftRightMove = 0; } else if (e.key === "ArrowDown") { upDownMove = 0; } else if (e.key === "ArrowUp") { upDownMove = 0; } }); } function stop_gn() { gn.stop(); } function start_gn() { gn.start(gnCallBack); } function gnCallBack(data) { if (!headSet) { gammaReference = data.do.gamma; } else { // leftRight = data.do.beta > 0 ? "right" : "left"; leftRightMove = data.do.beta; // upDown = data.do.gamma > gammaReference ? "up" : "down"; upDownMove = data.do.gamma - gammaReference; } } function set_head_gn() { try { gn.setHeadDirection(); } catch (e) { // fails if the sensor is not available } headSet = true; } init_gn(); // setup three.js: materials, geometries, scene, lighting and camera let scene, renderer; scene = new THREE.Scene(); const camera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 0.1, 5000); // obstacle geometry and material const geometrySphere = new THREE.SphereGeometry(11, 32, 32); const materialSphere = new THREE.MeshBasicMaterial(); const customMaterial = new THREE.ShaderMaterial({ uniforms: { "c": { type: "f", value: 0.8 }, "p": { type: "f", value: 2.4 }, viewVector: { type: "v3", value: camera.position } }, vertexShader: vertexShaderGlow, fragmentShader: fragmentShaderGlow, side: THREE.FrontSide, blending: THREE.AdditiveBlending, transparent: true }); // wall texture const loader = new THREE.TextureLoader(); const texture = loader.load('./bayer.png'); texture.minFilter = THREE.NearestFilter; texture.magFilter = THREE.NearestFilter; texture.wrapS = THREE.RepeatWrapping; texture.wrapT = THREE.RepeatWrapping; const uniforms = { iTime: { value: 0 }, iResolution: { value: new THREE.Vector3(1, 1, 1) }, iChannel0: { value: texture }, }; const material = new THREE.ShaderMaterial({ vertexShader, fragmentShader, uniforms, }); const borderGeometry = new THREE.PlaneGeometry(1, 1); init(); animate(); document.getElementById("start").onclick = () => { set_head_gn(); try { document.body.requestFullscreen(); window.screen.orientation.lock.call(window.screen.orientation, 'landscape'); } catch (e) { // browser doesn't support this API, try another hack window.scrollTo(0,1); } document.getElementById("start").style.zIndex = -10; document.getElementById("start").style.visibility = "hidden"; // reset variables speed = initialSpeed; score = 0; for (let i = 0; i < spheres.length; i++) { scene.remove(spheres[i]); } spheres.length = 0; for (let i = 0; i < borders.length; i++) { scene.remove(borders[i]); } borders.length = 0; removed = 0; lastSpawned = 0; camera.position.set(0, 10, 25); camera.lookAt(scene.position); running = true; } function gameOver() { running = false; headSet = false; document.getElementById("score").innerText = document.getElementById("score").innerText + " - Game Over!"; document.getElementById("start").style.zIndex = 0; document.getElementById("start").style.visibility = "visible"; document.getElementById("start").innerText = "Restart"; } function init() { camera.position.set(0, 10, 25); camera.lookAt(scene.position); const ambientLight = new THREE.AmbientLight(0xffffff, 0.7); scene.add(ambientLight); const pointLight = new THREE.PointLight(0xffffff, 0.8); camera.add(pointLight); scene.add(camera); renderer = new THREE.WebGLRenderer(); renderer.setPixelRatio(window.devicePixelRatio); renderer.setSize(window.innerWidth, window.innerHeight); document.body.appendChild(renderer.domElement); window.addEventListener('resize', onWindowResize); } function onWindowResize() { camera.aspect = window.innerWidth / window.innerHeight; camera.updateProjectionMatrix(); renderer.setSize(window.innerWidth, window.innerHeight); } // main render/update function called once per frame function animate() { let now = Date.now() / 1000.0; let delta = now - time; time = now; averageFps = (4 * averageFps + 1.0 / delta ) / 5.0; requestAnimationFrame(animate); uniforms.iTime.value += delta; if (running) { if (Math.abs(camera.position.x) > tunnelRadius || Math.abs(camera.position.y) > tunnelRadius) { // out of bounds gameOver(); } // collision checks for (let i = 0; i < spheres.length; i++) { if (spheres[i].material !== materialSphere) { continue; } let x1 = spheres[i].position.x; let y1 = spheres[i].position.y; let z1 = spheres[i].position.z; let x2 = camera.position.x; let y2 = camera.position.y; let z2 = camera.position.z; if (z2 > z1) { // account for very fast speeds // (prevents clipping through obstacles) z2 = Math.max(z1, z2 - speed); } let dist_squared = (x1 - x2) ** 2 + (y1 - y2) ** 2 + (z1 - z2) ** 2; if (dist_squared <= (spheres[i].geometry.parameters.radius * spheres[i].scale.x) ** 2) { gameOver(); break; } } } if (running) { // advance player position, increase speed, handle movement input camera.position.z -= 0.5 * speed * delta * 60.0; speed += 0.01; if (!Number.isNaN(leftRightMove)) { camera.position.x += 0.1 * leftRightMove; } if (!Number.isNaN(upDownMove)) { camera.position.y += 0.1 * upDownMove; } // create new obstacles as needed while (camera.position.z < lastSpawned - spawnInterval) { lastSpawned -= spawnInterval; // randomly spawn large spheres let scale = Math.random() < 0.1 ? 5.0 : 1.0; const meshCube = new THREE.Mesh(geometrySphere, materialSphere); meshCube.position.z = camera.position.z - speed * 210; meshCube.position.x = (2 * Math.random() - 1.0) * tunnelRadius; meshCube.position.y = (2 * Math.random() - 1.0) * 0.9 * tunnelRadius; meshCube.scale.multiplyScalar(scale); const outlineMesh = new THREE.Mesh(geometrySphere, customMaterial); outlineMesh.scale.multiplyScalar(scale * 1.17); outlineMesh.position.z = meshCube.position.z; outlineMesh.position.x = meshCube.position.x; outlineMesh.position.y = meshCube.position.y; meshCube.add(outlineMesh); scene.add(outlineMesh); spheres.push(outlineMesh); scene.add(meshCube); spheres.push(meshCube); } } renderer.render(scene, camera); // clean up game elements behind the camera for (let i = 0; i < spheres.length; i++) { if (spheres[i].position.z > camera.position.z + spheres[i].geometry.parameters.radius * spheres[i].scale.x + 20) { scene.remove(spheres[i]); spheres.splice(i, 1); i--; removed++; } } if (removed >= 2 || score == 0) { score += removed / 2; removed = 0; document.getElementById("score").innerText = score; } if (averageFps < 30.0 && renderer.getPixelRatio() == window.devicePixelRatio) { renderer.setPixelRatio(window.devicePixelRatio / 2); } // create new wall segments on demand if (borders.length == 0 || borders[borders.length - 1].position.z - camera.position.z > -1200) { const newZ = borders.length == 0 ? -200 : borders[borders.length - 1].position.z - tunnelRadius * 2; let border = new THREE.Mesh(borderGeometry, material); border.position.y = -tunnelRadius; border.rotation.x = -Math.PI / 2; border.position.z = newZ; border.scale.multiplyScalar(tunnelRadius * 2); scene.add(border); borders.push(border); border = new THREE.Mesh(borderGeometry, material); border.position.y = tunnelRadius; border.rotation.x = Math.PI / 2; border.position.z = newZ; border.scale.multiplyScalar(tunnelRadius * 2); scene.add(border); borders.push(border); border = new THREE.Mesh(borderGeometry, material); border.position.x = -tunnelRadius; border.rotation.y = Math.PI / 2; border.position.z = newZ; border.scale.multiplyScalar(tunnelRadius * 2); scene.add(border); borders.push(border); border = new THREE.Mesh(borderGeometry, material); border.position.x = tunnelRadius; border.rotation.y = -Math.PI / 2; border.position.z = newZ; border.scale.multiplyScalar(tunnelRadius * 2); scene.add(border); borders.push(border); } // clean up wall segments behind the camera for (let i = 0; i < borders.length; i++) { if (borders[i].position.z > camera.position.z + tunnelRadius + 50) { scene.remove(borders[i]); borders.splice(i, 1); i--; } } }