This commit is contained in:
2025-06-18 16:03:31 +03:00
parent ba3ac19704
commit 375b7663ee
9 changed files with 621 additions and 127 deletions
+8
View File
@@ -32,6 +32,7 @@
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.0.5",
"cannon-es": "^0.20.0",
"eslint": "^8.57.0",
"eslint-config-standard": "^17.1.0",
"eslint-config-vuetify": "^1.0.0",
@@ -2431,6 +2432,13 @@
"node": ">=6"
}
},
"node_modules/cannon-es": {
"version": "0.20.0",
"resolved": "https://registry.npmjs.org/cannon-es/-/cannon-es-0.20.0.tgz",
"integrity": "sha512-eZhWTZIkFOnMAJOgfXJa9+b3kVlvG+FX4mdkpePev/w/rP5V8NRquGyEozcjPfEoXUlb+p7d9SUcmDSn14prOA==",
"dev": true,
"license": "MIT"
},
"node_modules/chai": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/chai/-/chai-5.2.0.tgz",
+1
View File
@@ -34,6 +34,7 @@
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.0.5",
"cannon-es": "^0.20.0",
"eslint": "^8.57.0",
"eslint-config-standard": "^17.1.0",
"eslint-config-vuetify": "^1.0.0",
@@ -19,7 +19,7 @@
</template>
<script>
import { GameEngine } from '@/lib/gameEngine.js';
import { GameEngine } from '@/lib/GameEngine.js';
let gameEngine = null;
export default{
+2 -1
View File
@@ -48,7 +48,7 @@
<script>
import { GameEngine } from '@/lib/gameEngine';
import { GameEngine } from '@/lib/GameEngine';
let gameEngine = null;
@@ -150,6 +150,7 @@ export default {
*/
async loadEnvironment(scene, target){
//await gameEngine.loadPanorama(`/asset/default/43.webp`);
gameEngine.activeObjects.clear();
await this.expandScenarioData(scene);
target.objects = target.objects || {};
let l = target.objects;
+12 -1
View File
@@ -17,6 +17,7 @@
<v-btn-toggle variant="tonal" v-model="store.prefs.xr.depthSense" class="ma-3" density="comfortable" color="green-darken-2">
<v-btn :value="true" icon="mdi-cube-outline"></v-btn>
</v-btn-toggle>
<v-btn icon="mdi-walk" @click="control"></v-btn>
</v-navigation-drawer>
<div class="container my-3 position-relative game-designer-canvas">
<div ref="target" @click="targetClick" @pointerdown="targetPointerDown"></div>
@@ -50,7 +51,8 @@
<script>
import { GameEngine } from '@/lib/gameEngine';
import { GameEngine } from '@/lib/GameEngine';
import { Hero } from '@/lib/Hero';
import { useAppStore } from '@/stores/app';
const store = useAppStore();
@@ -152,6 +154,7 @@ export default {
//await gameEngine.loadPanorama(`/asset/default/43.webp`);
await this.expandScenarioData(scene);
//gameEngine.activeObjects.scale.set(0.033, 0.033, 0.033)
gameEngine.activeObjects.clear();
target.objects = target.objects || {};
let l = target.objects;
if (this.scene.data.$environment.type == 'panorama2d'){
@@ -165,6 +168,10 @@ export default {
let gltf = await gameEngine.load(`/asset/default/${i.data.$go.asset.name}`);
this.setObjectAttributes(l, i.data, gltf, 10);
gameEngine.activeObjects.add(gltf.scene);
if (i.data.$go.type == 'player3d'){
let hero = new Hero(gltf, i.data.$go);
hero.init(gameEngine);
}
//console.log(JSON.stringify(l));
//window.gameEngine = gameEngine;
//console.log(new gameEngine.$.Euler({"isEuler":true,"_x":0,"_y":0,"_z":0,"_order":"XYZ"}));
@@ -221,6 +228,10 @@ export default {
gameEngine.resize(r.clientWidth, r.clientHeight);
//this.zoom = Math.min(r.clientWidth / this.viewBox.w, r.clientHeight / this.viewBox.h);
},
control(){
gameEngine.hero.lockControls();
}
}
}
</script>
+73
View File
@@ -0,0 +1,73 @@
import { MeshBasicMaterial, TextureLoader, LinearFilter,
sRGBEncoding,
Mesh,
OrthographicCamera,
PlaneGeometry,
RGBAFormat,
Scene } from 'three';
import { Text } from 'troika-three-text';
class DashBoard {
constructor(renderer, width, height) {
var _camera = new OrthographicCamera(width / -2, width / 2, height / 2, height / -2, 0, 1);
var _scene = new Scene();
var _params = { minFilter: LinearFilter, magFilter: LinearFilter, format: RGBAFormat, stencilBuffer: true };
this.points = 0;
var _texture = new TextureLoader().load('./assets/maze/x.png');
_texture.encoding = sRGBEncoding;
var _material = new MeshBasicMaterial({
map: _texture,
alphaTest: .5
});
// _mesh = new Mesh( new PlaneGeometry( width * 0.015, width * 0.015 ), _material );
var _text = new Text();
_text.font = './assets/fonts/MonomakhUnicode.otf';
_text.text = 'Точки: 0';
_text.anchorX = 'right';
_text.anchorY = 'top';
_text.fontSize = width * 0.015;
_text.position.set(width * .48, height * .47, 0);
_text.color = 0xffffff;
_text.outlineColor = 0x222222;
_text.outlineWidth = '5%';
_text.outlineBlur = '5%';
//_scene.add( _mesh );
_scene.add(_text);
_text.sync();
this.render = function (scene, camera) {
renderer.render(_scene, _camera);
};
this.setSize = function (width, height) {
_camera.left = width / -2;
_camera.right = width / 2;
_camera.top = height / 2;
_camera.bottom = height / -2;
_camera.updateProjectionMatrix();
_text.position.set(width * .48, height * .47, 0);
};
this.addPoints = function (points) {
this.onpoints && this.onpoints(this.points + points, this.points);
this.points += points;
_text.text = 'точки: ' + this.points;
};
this.dispose = function () {
if (_mesh) _mesh.geometry.dispose();
if (_material) _material.dispose();
};
}
}
export { DashBoard };
+183
View File
@@ -0,0 +1,183 @@
import { AnimationMixer } from 'three';
import { PointerControls } from './PointerControls';
import * as THREE from 'three';
import * as CANNON from 'cannon-es';
class Hero{
constructor(object, data){
this.object = object;
this.data = data;
this.model = object.scene
}
init(gameEngine){
this.gameEngine = gameEngine;
this.mixer = new AnimationMixer(this.model);
gameEngine.mixers.push( this.mixer );
this.actionWalk = this.mixer.clipAction( this.object.animations.find(a=>a.name=='walk') );
this.actionIdle = this.mixer.clipAction( this.object.animations.find(a=>a.name=='idle') );
this.actionIdle.play();
this.activeAction = this.actionIdle;
this.pointerControls = new PointerControls(gameEngine.cameraPivot, this.model, gameEngine.renderer.domElement);
gameEngine.hero = this;
// Character Collider
const characterCollider = new THREE.Object3D()
characterCollider.position.y = 3
gameEngine.activeObjects.add(characterCollider)
const colliderShape = new CANNON.Sphere(0.5)
const colliderBody = new CANNON.Body({ mass: 1, material: gameEngine.phy.slipperyMaterial })
colliderBody.addShape(colliderShape, new CANNON.Vec3(0, 0.5, 0))
colliderBody.addShape(colliderShape, new CANNON.Vec3(0, -0.5, 0))
colliderBody.position.set(
characterCollider.position.x,
characterCollider.position.y,
characterCollider.position.z
)
colliderBody.linearDamping = 0.95
colliderBody.angularFactor.set(0, 1, 0) // prevents rotation X,Z axis
gameEngine.phy.world.addBody(colliderBody)
this.characterCollider = characterCollider;
this.colliderBody = colliderBody;
this.clock = new THREE.Clock()
this.delta = 0
this.inputVelocity = new THREE.Vector3()
this.velocity = new CANNON.Vec3()
this.euler = new THREE.Euler()
this.quat = new THREE.Quaternion()
this.v = new THREE.Vector3()
this.targetQuaternion = new THREE.Quaternion()
this.distance = 0
this.canJump = true;
this.contactNormal = new CANNON.Vec3()
this.upAxis = new CANNON.Vec3(0, 1, 0)
colliderBody.addEventListener('collide', function (e) {
const contact = e.contact
if (contact.bi.id == this.colliderBody.id) {
contact.ni.negate(this.contactNormal)
} else {
this.contactNormal.copy(contact.ni)
}
if (this.contactNormal.dot(this.upAxis) > 0.5) {
if (!this.canJump) {
setAction(animationActions[1], true)
}
this.canJump = true
}
}.bind(this))
this.ready = true;
}
lockControls(){
this.pointerControls.controls.lock();
}
setAction(toAction, loop){
if (toAction != this.activeAction) {
let lastAction = this.activeAction;
this.activeAction = toAction
lastAction.fadeOut(0.1)
this.activeAction.reset()
this.activeAction.fadeIn(0.1)
this.activeAction.play()
if (!loop) {
this.activeAction.clampWhenFinished = true
this.activeAction.loop = THREE.LoopOnce
}
}
}
update(){
let { inputVelocity, velocity, euler, quat, v, targetQuaternion, clock } = this;
let pc = this.pointerControls;
pc.update();
if (this.ready) {
if (this.canJump) {
//walking
this.mixer.update(this.distance / 100)
} else {
//were in the air
this.mixer.update(this.delta)
}
const p = this.characterCollider.position
p.y -= 1
this.model.position.y = this.characterCollider.position.y
this.distance = this.model.position.distanceTo(p)
const rotationMatrix = new THREE.Matrix4()
rotationMatrix.lookAt(p, this.model.position, this.model.up)
targetQuaternion.setFromRotationMatrix(rotationMatrix)
if (!this.model.quaternion.equals(targetQuaternion)) {
this.model.quaternion.rotateTowards(targetQuaternion, this.delta * 10)
}
if (this.canJump) {
inputVelocity.set(0, 0, 0)
if (pc.moveForward) {
inputVelocity.z = -1
}
if (pc.moveBackward) {
inputVelocity.z = 1
}
if (pc.moveLeft) {
inputVelocity.x = -1
}
if (pc.moveRight) {
inputVelocity.x = 1
}
inputVelocity.setLength(this.delta * 100)
// apply camera rotation to inputVelocity
euler.y = this.gameEngine.cameraYaw.rotation.y;
//euler.y = this.gameEngine.camera.rotation.y;
euler.order = 'XYZ'
quat.setFromEuler(euler)
inputVelocity.applyQuaternion(quat)
}
if(inputVelocity.x || inputVelocity.z){
this.setAction(this.actionWalk, true)
}else{
this.setAction(this.actionIdle, true)
}
this.model.position.lerp(this.characterCollider.position, 1)
}
velocity.set(inputVelocity.x, inputVelocity.y, inputVelocity.z)
this.colliderBody.applyImpulse(velocity)
this.delta = Math.min(clock.getDelta(), 0.1)
this.gameEngine.phy.world.step(this.delta)
//cannonDebugRenderer.update()
this.characterCollider.position.set(this.colliderBody.position.x, this.colliderBody.position.y, this.colliderBody.position.z)
// boxes.forEach((b, i) => {
// boxMeshes[i].position.set(b.position.x, b.position.y, b.position.z)
// boxMeshes[i].quaternion.set(b.quaternion.x, b.quaternion.y, b.quaternion.z, b.quaternion.w)
// })
this.characterCollider.getWorldPosition(v)
this.gameEngine.cameraPivot.position.lerp(v, 0.1)
// if (this.gameEngine.camera.position.distanceTo(v) > 200){
// this.gameEngine.camera.position.set(v.x+10, 3, v.z + 30);
// }
//this.gameEngine.camera.lookAt(v.x, 3, v.z);
//this.gameEngine.camera.rotation.y = this.model.rotation.y;
}
}
export { Hero }
+168
View File
@@ -0,0 +1,168 @@
import {
Clock,
Vector3
} from 'three';
import { PointerLockControls } from 'three/examples/jsm/Addons.js';
class PointerControls {
constructor(camera, hero, domElement) {
this.moveForward = false;
this.moveBackward = false;
this.moveLeft = false;
this.moveRight = false;
this.moveUp = false;
this.moveDown = false;
this.rotateLeft = false;
this.rotateRight = false;
this.canJump = false;
this.velocity = new Vector3();
this.direction = new Vector3();
this.rotationY = 0;
this.vertex = new Vector3();
this.rvelo = 0;
this.camera = camera;
this.hero = hero;
this.clock = new Clock();
this.click = false;
this.controls = new PointerLockControls(camera, domElement);
const onKeyDown = (event) => {
switch (event.code) {
case 'ArrowUp':
case 'KeyW':
this.moveForward = true;
break;
case 'ArrowLeft':
case 'KeyA':
this.moveLeft = true;
this.rotateLeft = true;
break;
case 'ArrowDown':
case 'KeyS':
this.moveBackward = true;
break;
case 'ArrowRight':
case 'KeyD':
this.moveRight = true;
this.rotateRight = true;
break;
case 'KeyQ':
this.rotateLeft = true;
break;
case 'KeyE':
this.rotateRight = true;
break;
case 'KeyR':
this.moveUp = true;
break;
case 'KeyF':
this.moveDown = true;
break;
case 'Space':
if (this.canJump === true) this.velocity.y += 350;
this.canJump = false;
break;
}
};
const onKeyUp = (event) => {
switch (event.code) {
case 'ArrowUp':
case 'KeyW':
this.moveForward = false;
break;
case 'ArrowLeft':
case 'KeyA':
this.moveLeft = false;
this.rotateLeft = false;
break;
case 'ArrowDown':
case 'KeyS':
this.moveBackward = false;
break;
case 'ArrowRight':
case 'KeyD':
this.moveRight = false;
this.rotateRight = false;
break;
case 'KeyQ':
this.rotateLeft = false;
break;
case 'KeyE':
this.rotateRight = false;
break;
case 'KeyR':
this.moveUp = false;
break;
case 'KeyF':
this.moveDown = false;
break;
}
};
document.addEventListener('keydown', onKeyDown);
document.addEventListener('keyup', onKeyUp);
window.addEventListener("gamepadconnected", (e) => {
this.gp = navigator.getGamepads()[e.gamepad.index];
console.log("Gamepad connected", this.gp);
});
domElement.addEventListener('click', () => {
this.controls.isLocked && this.clicked && this.clicked();
});
domElement.addEventListener('mousedown', () => {
this.controls.isLocked && this.onpointer && this.onpointer('start');
});
domElement.addEventListener('mousemove', () => {
this.controls.isLocked && this.onpointer && this.onpointer('drag');
});
domElement.addEventListener('mouseup', () => {
this.controls.isLocked && this.onpointer && this.onpointer('end');
});
this.update = () => {
// const delta = this.clock.getDelta() * 10;
// if (this.gp) {
// this.gp = navigator.getGamepads()[this.gp.index];
// this.gp.pressed = this.gp.buttons[4].pressed || this.gp.buttons[5].pressed || this.gp.buttons[6].pressed || this.gp.buttons[7].pressed || this.gp.buttons[9].pressed;
// if (!this.click && this.gp.pressed) {
// this.click = true;
// this.clicked?.();
// //console.log(this.gp.buttons.map((b,i)=>[i, b.pressed]));
// }
// if (this.click && !this.gp.pressed) this.click = false;
// }
// //this.velocity.x -= this.velocity.x * 5.0 * delta;
// this.velocity.z -= this.velocity.z * 5.0 * delta;
// this.rvelo -= this.rvelo * 5 * delta;
// // this.velocity.y -= 9.8 * 100.0 * delta; // 100.0 = mass
// this.direction.z = Number(this.moveForward) - Number(this.moveBackward) - Math.floor((this.gp && this.gp.axes[1] || 0) * 100) / 100 + (this.touchControls && this.touchControls.move || 0);
// this.rotationY = Number(this.rotateLeft) - Number(this.rotateRight) - Math.floor((this.gp && this.gp.axes[0] || 0) * 100) / 110 + (this.touchControls && this.touchControls.rotate || 0);
// //this.direction.x = Number( this.moveRight ) - Number( this.moveLeft );
// //this.direction.normalize(); // this ensures consistent movements in all directions
// if (this.direction.z) this.velocity.z -= this.direction.z * 5.0 * delta;
// //if ( this.moveLeft || this.moveRight ) this.velocity.x -= this.direction.x * 5.0 * delta;
// if (this.rotationY) this.rvelo -= this.rotationY * 8 * delta;
//this.velocity.x && this.controls.moveRight( - this.velocity.x * delta );
// if (this.velocity.z) {
// this.controls.moveForward(-this.velocity.z * delta);
// this.hero.position.z += -this.velocity.z * delta;
// }
// this.controls.moveRight( this.direction.x * delta );
// this.controls.moveForward( this.direction.z * delta );
// this.hero.position.y += (Number(this.moveUp) - Number(this.moveDown)) * delta;
// this.hero.rotation.y += -this.rvelo * delta;
};
}
}
export { PointerControls };
+57 -8
View File
@@ -2,6 +2,7 @@ import * as THREE from 'three';
import { GLTFLoader } from 'three/examples/jsm/Addons.js';
import { DRACOLoader } from 'three/examples/jsm/Addons.js';
import { OrbitControls } from 'three/examples/jsm/Addons.js';
//import { Controller as OrbitControls } from './3rd-party/phy/3TH/Controller.js';
import { ViewportGizmo } from "three-viewport-gizmo";
//import { AnaglyphEffect } from './three/AnaglyphEffect';
import { AnaglyphEffect } from 'three/addons/effects/AnaglyphEffect.js';
@@ -12,6 +13,7 @@ import { TransformControls } from 'three/addons/controls/TransformControls.js';
import { ARButton } from 'three/addons/webxr/ARButton.js';
import { XRButton } from 'three/addons/webxr/XRButton.js';
import { XRControllerModelFactory } from 'three/addons/webxr/XRControllerModelFactory.js';
import * as CANNON from 'cannon-es';
class GameEngine {
async init(domNode, opts = {}) {
@@ -34,6 +36,9 @@ class GameEngine {
0.01, 1000);
this.orthographicCamera.position.set(0, 0, 100);
const scene = new THREE.Scene();
this.scene = scene;
this.initCameraPivot()
// let light = new THREE.AmbientLight( 0x404040, 300 ); // soft white light
// scene.add( this.light );
@@ -84,7 +89,10 @@ class GameEngine {
this.stereo = new StereoEffect(renderer);
this.stereo.setSize(this.w, this.h);
const controls = new OrbitControls( this.camera, renderer.domElement );
this.activeObjects = new THREE.Group();
scene.add(this.activeObjects);
const controls = new OrbitControls(this.camera, renderer.domElement, this.activeObjects);
if (opts.gizmo) {
const gizmo = new ViewportGizmo(this.camera, renderer, {
container: '.renderer-gizmo',
@@ -108,7 +116,8 @@ class GameEngine {
function animate(time) {
let delta = clock.getDelta();
mixer.update(delta);
gameEngine.hero?.update();
gameEngine.mixers.forEach(m => m.update(delta));
gameEngine.handleXrAction(gameEngine, delta)
gameEngine.render(scene, gameEngine.camera);
gameEngine.gizmo?.render();
@@ -120,15 +129,11 @@ class GameEngine {
this.clock = clock;
this.renderer = renderer;
this.scene = scene;
this.draco = new DRACOLoader().setDecoderPath('/3rdparty/draco/');
this.loader = new GLTFLoader();
this.loader.setDRACOLoader(this.draco);
this.mixer = mixer;
this.activeObjects = new THREE.Group();
scene.add(this.activeObjects);
this.mixers = [mixer];
domNode.appendChild(renderer.domElement);
@@ -147,6 +152,8 @@ class GameEngine {
gameEngine.camera.updateProjectionMatrix();
})
this.initPhysics();
if (opts.ar) {
renderer.xr.enabled = true;
document.body.appendChild(ARButton.createButton(renderer, {}));
@@ -210,6 +217,48 @@ class GameEngine {
this.xrController2 = c2
}
initCameraPivot() {
const pivot = new THREE.Object3D()
pivot.position.set(0, 1, 10)
const yaw = new THREE.Object3D()
const pitch = new THREE.Object3D()
this.scene.add(pivot)
pivot.add(yaw)
yaw.add(pitch)
pitch.add(this.perspectiveCamera);
pitch.add(this.orthographicCamera);
this.cameraPivot = pivot;
this.cameraYaw = yaw;
}
initPhysics() {
const world = new CANNON.World()
world.gravity.set(0, -9.82, 0)
const groundMaterial = new CANNON.Material('groundMaterial')
const slipperyMaterial = new CANNON.Material('slipperyMaterial')
const slippery_ground_cm = new CANNON.ContactMaterial(
groundMaterial,
slipperyMaterial,
{
friction: 0,
restitution: 0.3,
contactEquationStiffness: 1e8,
contactEquationRelaxation: 3,
}
)
world.addContactMaterial(slippery_ground_cm)
const planeShape = new CANNON.Plane()
const planeBody = new CANNON.Body({ mass: 0, material: groundMaterial })
planeBody.addShape(planeShape)
planeBody.quaternion.setFromAxisAngle(new CANNON.Vec3(1, 0, 0), -Math.PI / 2)
world.addBody(planeBody)
this.phy = {
world, slipperyMaterial
}
}
handleXrAction(gameEngine, delta) {
//console.log(event.type);
if (gameEngine.xrController1?.gamepad) {
@@ -342,7 +391,7 @@ class GameEngine {
}
playAnimation(object, clip, play = true) {
let action = this.mixer.clipAction(clip, object);
let action = this.mixers[0].clipAction(clip, object);
if (play) action.play();
else action.stop();
}