123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483 |
- import * as THREE from 'https://cdn.skypack.dev/[email protected]';
- import {FirstPersonControls} from 'https://cdn.skypack.dev/[email protected]/examples/jsm/controls/FirstPersonControls.js';
- const KEYS = {
- 'a': 65,
- 's': 83,
- 'w': 87,
- 'd': 68,
- };
- function clamp(x, a, b) {
- return Math.min(Math.max(x, a), b);
- }
- class InputController {
- constructor(target) {
- this.target_ = target || document;
- this.initialize_();
- }
- initialize_() {
- this.current_ = {
- leftButton: false,
- rightButton: false,
- mouseXDelta: 0,
- mouseYDelta: 0,
- mouseX: 0,
- mouseY: 0,
- };
- this.previous_ = null;
- this.keys_ = {};
- this.previousKeys_ = {};
- this.target_.addEventListener('mousedown', (e) => this.onMouseDown_(e), false);
- this.target_.addEventListener('mousemove', (e) => this.onMouseMove_(e), false);
- this.target_.addEventListener('mouseup', (e) => this.onMouseUp_(e), false);
- this.target_.addEventListener('keydown', (e) => this.onKeyDown_(e), false);
- this.target_.addEventListener('keyup', (e) => this.onKeyUp_(e), false);
- }
- onMouseMove_(e) {
- this.current_.mouseX = e.pageX - window.innerWidth / 2;
- this.current_.mouseY = e.pageY - window.innerHeight / 2;
- if (this.previous_ === null) {
- this.previous_ = {...this.current_};
- }
- this.current_.mouseXDelta = this.current_.mouseX - this.previous_.mouseX;
- this.current_.mouseYDelta = this.current_.mouseY - this.previous_.mouseY;
- }
- onMouseDown_(e) {
- this.onMouseMove_(e);
- switch (e.button) {
- case 0: {
- this.current_.leftButton = true;
- break;
- }
- case 2: {
- this.current_.rightButton = true;
- break;
- }
- }
- }
- onMouseUp_(e) {
- this.onMouseMove_(e);
- switch (e.button) {
- case 0: {
- this.current_.leftButton = false;
- break;
- }
- case 2: {
- this.current_.rightButton = false;
- break;
- }
- }
- }
- onKeyDown_(e) {
- this.keys_[e.keyCode] = true;
- }
- onKeyUp_(e) {
- this.keys_[e.keyCode] = false;
- }
- key(keyCode) {
- return !!this.keys_[keyCode];
- }
- isReady() {
- return this.previous_ !== null;
- }
- update(_) {
- if (this.previous_ !== null) {
- this.current_.mouseXDelta = this.current_.mouseX - this.previous_.mouseX;
- this.current_.mouseYDelta = this.current_.mouseY - this.previous_.mouseY;
- this.previous_ = {...this.current_};
- }
- }
- };
- class FirstPersonCamera {
- constructor(camera, objects) {
- this.camera_ = camera;
- this.input_ = new InputController();
- this.rotation_ = new THREE.Quaternion();
- this.translation_ = new THREE.Vector3(0, 2, 0);
- this.phi_ = 0;
- this.phiSpeed_ = 8;
- this.theta_ = 0;
- this.thetaSpeed_ = 5;
- this.headBobActive_ = false;
- this.headBobTimer_ = 0;
- this.objects_ = objects;
- }
- update(timeElapsedS) {
- this.updateRotation_(timeElapsedS);
- this.updateCamera_(timeElapsedS);
- this.updateTranslation_(timeElapsedS);
- this.updateHeadBob_(timeElapsedS);
- this.input_.update(timeElapsedS);
- }
- updateCamera_(_) {
- this.camera_.quaternion.copy(this.rotation_);
- this.camera_.position.copy(this.translation_);
- this.camera_.position.y += Math.sin(this.headBobTimer_ * 10) * 1.5;
- const forward = new THREE.Vector3(0, 0, -1);
- forward.applyQuaternion(this.rotation_);
- const dir = forward.clone();
- forward.multiplyScalar(100);
- forward.add(this.translation_);
- let closest = forward;
- const result = new THREE.Vector3();
- const ray = new THREE.Ray(this.translation_, dir);
- for (let i = 0; i < this.objects_.length; ++i) {
- if (ray.intersectBox(this.objects_[i], result)) {
- if (result.distanceTo(ray.origin) < closest.distanceTo(ray.origin)) {
- closest = result.clone();
- }
- }
- }
- this.camera_.lookAt(closest);
- }
- updateHeadBob_(timeElapsedS) {
- if (this.headBobActive_) {
- const wavelength = Math.PI;
- const nextStep = 1 + Math.floor(((this.headBobTimer_ + 0.000001) * 10) / wavelength);
- const nextStepTime = nextStep * wavelength / 10;
- this.headBobTimer_ = Math.min(this.headBobTimer_ + timeElapsedS, nextStepTime);
- if (this.headBobTimer_ == nextStepTime) {
- this.headBobActive_ = false;
- }
- }
- }
- updateTranslation_(timeElapsedS) {
- const forwardVelocity = (this.input_.key(KEYS.w) ? 1 : 0) + (this.input_.key(KEYS.s) ? -1 : 0)
- const strafeVelocity = (this.input_.key(KEYS.a) ? 1 : 0) + (this.input_.key(KEYS.d) ? -1 : 0)
- const qx = new THREE.Quaternion();
- qx.setFromAxisAngle(new THREE.Vector3(0, 1, 0), this.phi_);
- const forward = new THREE.Vector3(0, 0, -1);
- forward.applyQuaternion(qx);
- forward.multiplyScalar(forwardVelocity * timeElapsedS * 10);
- const left = new THREE.Vector3(-1, 0, 0);
- left.applyQuaternion(qx);
- left.multiplyScalar(strafeVelocity * timeElapsedS * 10);
- this.translation_.add(forward);
- this.translation_.add(left);
- if (forwardVelocity != 0 || strafeVelocity != 0) {
- this.headBobActive_ = true;
- }
- }
- updateRotation_(timeElapsedS) {
- const xh = this.input_.current_.mouseXDelta / window.innerWidth;
- const yh = this.input_.current_.mouseYDelta / window.innerHeight;
- this.phi_ += -xh * this.phiSpeed_;
- this.theta_ = clamp(this.theta_ + -yh * this.thetaSpeed_, -Math.PI / 3, Math.PI / 3);
- const qx = new THREE.Quaternion();
- qx.setFromAxisAngle(new THREE.Vector3(0, 1, 0), this.phi_);
- const qz = new THREE.Quaternion();
- qz.setFromAxisAngle(new THREE.Vector3(1, 0, 0), this.theta_);
- const q = new THREE.Quaternion();
- q.multiply(qx);
- q.multiply(qz);
- this.rotation_.copy(q);
- }
- }
- class FirstPersonCameraDemo {
- constructor() {
- this.initialize_();
- }
- initialize_() {
- this.initializeRenderer_();
- this.initializeLights_();
- this.initializeScene_();
- this.initializePostFX_();
- this.initializeDemo_();
- this.previousRAF_ = null;
- this.raf_();
- this.onWindowResize_();
- }
- initializeDemo_() {
- // this.controls_ = new FirstPersonControls(
- // this.camera_, this.threejs_.domElement);
- // this.controls_.lookSpeed = 0.8;
- // this.controls_.movementSpeed = 5;
- this.fpsCamera_ = new FirstPersonCamera(this.camera_, this.objects_);
- }
- initializeRenderer_() {
- this.threejs_ = new THREE.WebGLRenderer({
- antialias: false,
- });
- this.threejs_.shadowMap.enabled = true;
- this.threejs_.shadowMap.type = THREE.PCFSoftShadowMap;
- this.threejs_.setPixelRatio(window.devicePixelRatio);
- this.threejs_.setSize(window.innerWidth, window.innerHeight);
- this.threejs_.physicallyCorrectLights = true;
- this.threejs_.outputEncoding = THREE.sRGBEncoding;
- document.body.appendChild(this.threejs_.domElement);
- window.addEventListener('resize', () => {
- this.onWindowResize_();
- }, false);
- const fov = 60;
- const aspect = 1920 / 1080;
- const near = 1.0;
- const far = 1000.0;
- this.camera_ = new THREE.PerspectiveCamera(fov, aspect, near, far);
- this.camera_.position.set(0, 2, 0);
- this.scene_ = new THREE.Scene();
- this.uiCamera_ = new THREE.OrthographicCamera(
- -1, 1, 1 * aspect, -1 * aspect, 1, 1000);
- this.uiScene_ = new THREE.Scene();
- }
- initializeScene_() {
- const loader = new THREE.CubeTextureLoader();
- const texture = loader.load([
- './resources/skybox/posx.jpg',
- './resources/skybox/negx.jpg',
- './resources/skybox/posy.jpg',
- './resources/skybox/negy.jpg',
- './resources/skybox/posz.jpg',
- './resources/skybox/negz.jpg',
- ]);
- texture.encoding = THREE.sRGBEncoding;
- this.scene_.background = texture;
- const mapLoader = new THREE.TextureLoader();
- const maxAnisotropy = this.threejs_.capabilities.getMaxAnisotropy();
- const checkerboard = mapLoader.load('resources/checkerboard.png');
- checkerboard.anisotropy = maxAnisotropy;
- checkerboard.wrapS = THREE.RepeatWrapping;
- checkerboard.wrapT = THREE.RepeatWrapping;
- checkerboard.repeat.set(32, 32);
- checkerboard.encoding = THREE.sRGBEncoding;
- const plane = new THREE.Mesh(
- new THREE.PlaneGeometry(100, 100, 10, 10),
- new THREE.MeshStandardMaterial({map: checkerboard}));
- plane.castShadow = false;
- plane.receiveShadow = true;
- plane.rotation.x = -Math.PI / 2;
- this.scene_.add(plane);
- const box = new THREE.Mesh(
- new THREE.BoxGeometry(4, 4, 4),
- this.loadMaterial_('vintage-tile1_', 0.2));
- box.position.set(10, 2, 0);
- box.castShadow = true;
- box.receiveShadow = true;
- this.scene_.add(box);
- const concreteMaterial = this.loadMaterial_('concrete3-', 4);
- const wall1 = new THREE.Mesh(
- new THREE.BoxGeometry(100, 100, 4),
- concreteMaterial);
- wall1.position.set(0, -40, -50);
- wall1.castShadow = true;
- wall1.receiveShadow = true;
- this.scene_.add(wall1);
- const wall2 = new THREE.Mesh(
- new THREE.BoxGeometry(100, 100, 4),
- concreteMaterial);
- wall2.position.set(0, -40, 50);
- wall2.castShadow = true;
- wall2.receiveShadow = true;
- this.scene_.add(wall2);
- const wall3 = new THREE.Mesh(
- new THREE.BoxGeometry(4, 100, 100),
- concreteMaterial);
- wall3.position.set(50, -40, 0);
- wall3.castShadow = true;
- wall3.receiveShadow = true;
- this.scene_.add(wall3);
- const wall4 = new THREE.Mesh(
- new THREE.BoxGeometry(4, 100, 100),
- concreteMaterial);
- wall4.position.set(-50, -40, 0);
- wall4.castShadow = true;
- wall4.receiveShadow = true;
- this.scene_.add(wall4);
- // Create Box3 for each mesh in the scene so that we can
- // do some easy intersection tests.
- const meshes = [
- plane, box, wall1, wall2, wall3, wall4];
- this.objects_ = [];
- for (let i = 0; i < meshes.length; ++i) {
- const b = new THREE.Box3();
- b.setFromObject(meshes[i]);
- this.objects_.push(b);
- }
- // Crosshair
- const crosshair = mapLoader.load('resources/crosshair.png');
- crosshair.anisotropy = maxAnisotropy;
- this.sprite_ = new THREE.Sprite(
- new THREE.SpriteMaterial({map: crosshair, color: 0xffffff, fog: false, depthTest: false, depthWrite: false}));
- this.sprite_.scale.set(0.15, 0.15 * this.camera_.aspect, 1)
- this.sprite_.position.set(0, 0, -10);
- this.uiScene_.add(this.sprite_);
- }
- initializeLights_() {
- const distance = 50.0;
- const angle = Math.PI / 4.0;
- const penumbra = 0.5;
- const decay = 1.0;
- let light = new THREE.SpotLight(
- 0xFFFFFF, 100.0, distance, angle, penumbra, decay);
- light.castShadow = true;
- light.shadow.bias = -0.00001;
- light.shadow.mapSize.width = 4096;
- light.shadow.mapSize.height = 4096;
- light.shadow.camera.near = 1;
- light.shadow.camera.far = 100;
- light.position.set(25, 25, 0);
- light.lookAt(0, 0, 0);
- this.scene_.add(light);
- const upColour = 0xFFFF80;
- const downColour = 0x808080;
- light = new THREE.HemisphereLight(upColour, downColour, 0.5);
- light.color.setHSL( 0.6, 1, 0.6 );
- light.groundColor.setHSL( 0.095, 1, 0.75 );
- light.position.set(0, 4, 0);
- this.scene_.add(light);
- }
- loadMaterial_(name, tiling) {
- const mapLoader = new THREE.TextureLoader();
- const maxAnisotropy = this.threejs_.capabilities.getMaxAnisotropy();
- const metalMap = mapLoader.load('resources/freepbr/' + name + 'metallic.png');
- metalMap.anisotropy = maxAnisotropy;
- metalMap.wrapS = THREE.RepeatWrapping;
- metalMap.wrapT = THREE.RepeatWrapping;
- metalMap.repeat.set(tiling, tiling);
- const albedo = mapLoader.load('resources/freepbr/' + name + 'albedo.png');
- albedo.anisotropy = maxAnisotropy;
- albedo.wrapS = THREE.RepeatWrapping;
- albedo.wrapT = THREE.RepeatWrapping;
- albedo.repeat.set(tiling, tiling);
- albedo.encoding = THREE.sRGBEncoding;
- const normalMap = mapLoader.load('resources/freepbr/' + name + 'normal.png');
- normalMap.anisotropy = maxAnisotropy;
- normalMap.wrapS = THREE.RepeatWrapping;
- normalMap.wrapT = THREE.RepeatWrapping;
- normalMap.repeat.set(tiling, tiling);
- const roughnessMap = mapLoader.load('resources/freepbr/' + name + 'roughness.png');
- roughnessMap.anisotropy = maxAnisotropy;
- roughnessMap.wrapS = THREE.RepeatWrapping;
- roughnessMap.wrapT = THREE.RepeatWrapping;
- roughnessMap.repeat.set(tiling, tiling);
- const material = new THREE.MeshStandardMaterial({
- metalnessMap: metalMap,
- map: albedo,
- normalMap: normalMap,
- roughnessMap: roughnessMap,
- });
- return material;
- }
- initializePostFX_() {
- }
- onWindowResize_() {
- this.camera_.aspect = window.innerWidth / window.innerHeight;
- this.camera_.updateProjectionMatrix();
- this.uiCamera_.left = -this.camera_.aspect;
- this.uiCamera_.right = this.camera_.aspect;
- this.uiCamera_.updateProjectionMatrix();
- this.threejs_.setSize(window.innerWidth, window.innerHeight);
- }
- raf_() {
- requestAnimationFrame((t) => {
- if (this.previousRAF_ === null) {
- this.previousRAF_ = t;
- }
- this.step_(t - this.previousRAF_);
- this.threejs_.autoClear = true;
- this.threejs_.render(this.scene_, this.camera_);
- this.threejs_.autoClear = false;
- this.threejs_.render(this.uiScene_, this.uiCamera_);
- this.previousRAF_ = t;
- this.raf_();
- });
- }
- step_(timeElapsed) {
- const timeElapsedS = timeElapsed * 0.001;
- // this.controls_.update(timeElapsedS);
- this.fpsCamera_.update(timeElapsedS);
- }
- }
- let _APP = null;
- window.addEventListener('DOMContentLoaded', () => {
- _APP = new FirstPersonCameraDemo();
- });
|