// [WHALE BY FIPS @ 4FIPS.COM, (c) 2018 FILIP STOKLAS, MIT-LICENSED]
if (Detector.webgl) {
render_loop(init());
}
function init() {
const scene = new THREE.Scene();
scene.background = new THREE.Color(0xffffff);
scene.fog = new THREE.FogExp2(0xffffff, 0.09);
const camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.25, 30);
camera.position.set(.0, .0, 10);
const light = new THREE.HemisphereLight(0xffffff, 0xeeeeff);
light.position.set(0, 1, 0);
scene.add(light);
const canvas = document.getElementById("whale_canvas");
renderer = new THREE.WebGLRenderer({ canvas: canvas, antialias: true });
renderer.setPixelRatio(window.devicePixelRatio);
renderer.gammaOutput = true;
const state = {
clock: new THREE.Clock(),
scene: scene,
camera: camera,
canvas: canvas,
renderer: renderer,
model_dir: new THREE.Vector3(0, 0, 1),
target_pos: new THREE.Vector3(0, 3, 8),
follow_dur: 0,
model: null,
mixer: null,
}
// load model asynchronously
const loader = new THREE.GLTFLoader();
loader.load('scene/scene.gltf', function (gltf) {
state.model = gltf.scene;
scene.add(state.model);
state.model.position.set(0, -8, -3);
state.mixer = new THREE.AnimationMixer(state.model);
state.mixer.clipAction(gltf.animations[0]).play();
});
// install event handlers:
document.addEventListener('mousedown', function(){
change_target(state, (event.clientX / window.innerWidth) * 2 - 1, -(event.clientY / window.innerHeight) * 2 + 1, -8 + 3 * Math.random());
}, false);
document.addEventListener('touchstart', function(){
change_target(state, (event.clientX / window.innerWidth) * 2 - 1, -(event.clientY / window.innerHeight) * 2 + 1, -8 + 3 * Math.random());
}, false);
return state;
}
function resize(state) {
const width = state.canvas.clientWidth;
const height = state.canvas.clientHeight;
if (width != state.canvas.width || height != state.canvas.height) {
state.renderer.setSize(width, height, false);
state.camera.aspect = width / height;
state.camera.updateProjectionMatrix();
}
}
function render(state) {
resize(state);
const time = state.clock.elapsedTime;
const time_delta = state.clock.getDelta();
if (state.model && state.mixer) { // wait for the model
if (state.follow_dur > 12) {
change_target(state, -1.1 + 2.2 * Math.random(), -1.1 + 2.2 * Math.random(), -2 + 12 * Math.random());
}
state.follow_dur += time_delta;
state.mixer.update(.9 * time_delta);
// move towards the target
{
const speed = 0.015;
const pos_delta = state.model_dir.clone().multiplyScalar(speed);
state.model.position.add(pos_delta);
}
// orient towards the target
{
const inertia = 200 - Math.min(50 * state.follow_dur, 150);
const target_dir = state.target_pos.clone().sub(state.model.position).normalize();
state.model_dir.multiplyScalar(inertia).add(target_dir).normalize();
state.model.lookAt(state.model.position.clone().add(state.model_dir));
}
}
state.renderer.render(state.scene, state.camera);
}
function render_loop(state) {
resize(state);
render(state);
requestAnimationFrame(function(timestamp){ render_loop(state); });
}
function change_target(state, ndc_x, ndc_y, depth) { // normalized device coordinates <-1, 1>
const vec = new THREE.Vector3(ndc_x, ndc_y, .5);
const unpvec = vec.clone().unproject(state.camera);
const dir = unpvec.clone().sub(state.camera.position).normalize();
const distance = -state.camera.position.z / dir.z;
const rand = dir.clone().multiplyScalar(depth);
const pos = state.camera.position.clone().add(dir.clone().multiplyScalar(distance)).add(rand);
state.target_pos.copy(pos);
state.follow_dur = 0;
}