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
+173 -124
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 = {}) {
@@ -27,67 +29,73 @@ class GameEngine {
this.frustumSize = 50;
this.orthographicCamera = new THREE.OrthographicCamera(
this.frustumSize * this.aspect / - 2,
this.frustumSize * this.aspect / 2,
this.frustumSize / 2,
this.frustumSize / - 2,
this.frustumSize * this.aspect / - 2,
this.frustumSize * this.aspect / 2,
this.frustumSize / 2,
this.frustumSize / - 2,
0.01, 1000);
this.orthographicCamera.position.set( 0, 0, 100 );
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 );
const dirLight = new THREE.DirectionalLight( 0xffffff, 4 );
dirLight.color.setHSL( 0.1, 1, 0.95 );
dirLight.position.set( -12, 33, 37 );
const dirLight = new THREE.DirectionalLight(0xffffff, 4);
dirLight.color.setHSL(0.1, 1, 0.95);
dirLight.position.set(-12, 33, 37);
//dirLight.position.multiplyScalar( 20 );
scene.add( dirLight );
scene.add(dirLight);
dirLight.castShadow = true;
dirLight.shadow.mapSize.width = 2048;
dirLight.shadow.mapSize.height = 2048;
const d = 50;
dirLight.shadow.camera.left = - d;
dirLight.shadow.camera.right = d;
dirLight.shadow.camera.top = d;
dirLight.shadow.camera.bottom = - d;
dirLight.shadow.camera.far = 1000;
dirLight.shadow.bias = - 0.001;
const renderer = new THREE.WebGLRenderer({
antialias: true,
const renderer = new THREE.WebGLRenderer({
antialias: true,
// alpha: true,
preserveDrawingBuffer: true //this is important for screenshots capturing
//powerPreference: "high-performance",
});
renderer.setPixelRatio( window.devicePixelRatio );
renderer.setPixelRatio(window.devicePixelRatio);
// renderer.toneMapping = THREE.CineonToneMapping;
// renderer.toneMappingExposure = 1.2;
// renderer.shadowMap.enabled = true;
// renderer.shadowMap.type = THREE.PCFSoftShadowMap; // default THREE.PCFShadowMap
// renderer.toneMapping = THREE.ACESFilmicToneMapping;
// renderer.toneMappingExposure = 1;
// renderer.toneMappingExposure = 1;
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
renderer.setSize(this.w, this.h);
renderer.setViewport( 0, 0, this.w, this.h );
renderer.setViewport(0, 0, this.w, this.h);
renderer.autoClear = true;
this.anaglyph = new AnaglyphEffect( renderer );
this.anaglyph.setSize( this.w, this.h );
this.anaglyph = new AnaglyphEffect(renderer);
this.anaglyph.setSize(this.w, this.h);
this.stereo = new StereoEffect(renderer);
this.stereo.setSize( this.w, this.h );
this.stereo.setSize(this.w, this.h);
const controls = new OrbitControls( this.camera, renderer.domElement );
if (opts.gizmo){
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',
container: '.renderer-gizmo',
//type:'cube'
});
gizmo.attachControls(controls);
@@ -97,18 +105,19 @@ class GameEngine {
this.orbitControls = controls;
//controls.enableZoom = true;
//const controls = new MapControls( camera, renderer.domElement );
this.transformControls = new TransformControls( this.camera, renderer.domElement );
this.transformControls.addEventListener( 'dragging-changed', function ( event ) {
controls.enabled = ! event.value;
} );
this.transformControls = new TransformControls(this.camera, renderer.domElement);
this.transformControls.addEventListener('dragging-changed', function (event) {
controls.enabled = !event.value;
});
// controls.enableDamping = true;
// controls.screenSpacePanning = true;
this.renderType = 'ST';
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.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);
@@ -138,24 +143,26 @@ class GameEngine {
texture.mapping = THREE.EquirectangularReflectionMapping;
// scene.background = bck; //new THREE.Color(0.7,0.7,0.7);
scene.environment = texture;
scene.background = new THREE.Color(1,1,1);
scene.background = new THREE.Color(1, 1, 1);
console.log('GameEngine started')
renderer.domElement.addEventListener('wheel', (event)=>{
renderer.domElement.addEventListener('wheel', (event) => {
gameEngine.camera.zoom -= event.deltaY / 1000;
gameEngine.camera.zoom = Math.max(gameEngine.camera.zoom, .4);
controls.rotateSpeed = 1 / gameEngine.camera.zoom;
gameEngine.camera.updateProjectionMatrix();
})
if (opts.ar){
this.initPhysics();
if (opts.ar) {
renderer.xr.enabled = true;
document.body.appendChild( ARButton.createButton( renderer, { } ) );
document.body.appendChild(ARButton.createButton(renderer, {}));
}
if (opts.xr){
if (opts.xr) {
renderer.xr.enabled = true;
document.body.appendChild(XRButton.createButton(renderer, opts.depthSense ?{
document.body.appendChild(XRButton.createButton(renderer, opts.depthSense ? {
'requiredFeatures': ['depth-sensing'],
'depthSensing': {
'depthSensing': {
usagePreference: ["gpu-optimized"],
dataFormatPreference: ["unsigned-short"],
matchDepthView: false
@@ -165,44 +172,44 @@ class GameEngine {
}
}
initXrControllers(){
let c1 = this.renderer.xr.getController( 0 );
c1.addEventListener( 'select', this.onSelect.bind(this) );
c1.addEventListener( 'selectstart', this.onControllerEvent.bind(this) );
c1.addEventListener( 'selectend', this.onControllerEvent.bind(this) );
c1.addEventListener( 'move', this.onControllerEvent.bind(this) );
initXrControllers() {
let c1 = this.renderer.xr.getController(0);
c1.addEventListener('select', this.onSelect.bind(this));
c1.addEventListener('selectstart', this.onControllerEvent.bind(this));
c1.addEventListener('selectend', this.onControllerEvent.bind(this));
c1.addEventListener('move', this.onControllerEvent.bind(this));
c1.userData.active = false;
c1.addEventListener('connected', e=>{
c1.addEventListener('connected', e => {
c1.gamepad = e.data.gamepad;
this.session = this.renderer.xr.getSession();
this.session.addEventListener('selectstart', this.onControllerEvent.bind(this));
})
this.scene.add( c1 );
this.scene.add(c1);
let c2 = this.renderer.xr.getController( 1 );
c2.addEventListener( 'select', this.onSelect.bind(this) );
c2.addEventListener( 'selectstart', this.onControllerEvent.bind(this) );
c2.addEventListener( 'selectend', this.onControllerEvent.bind(this) );
c2.addEventListener( 'move', this.onControllerEvent.bind(this) );
let c2 = this.renderer.xr.getController(1);
c2.addEventListener('select', this.onSelect.bind(this));
c2.addEventListener('selectstart', this.onControllerEvent.bind(this));
c2.addEventListener('selectend', this.onControllerEvent.bind(this));
c2.addEventListener('move', this.onControllerEvent.bind(this));
c2.userData.active = true;
c2.addEventListener('connected', e=>{
c2.addEventListener('connected', e => {
c2.gamepad = e.data.gamepad;
})
this.scene.add( c2 );
this.scene.add(c2);
const controllerModelFactory = new XRControllerModelFactory();
let controllerGrip1 = this.renderer.xr.getControllerGrip( 0 );
controllerGrip1.add( controllerModelFactory.createControllerModel( controllerGrip1 ) );
this.scene.add( controllerGrip1 );
let controllerGrip1 = this.renderer.xr.getControllerGrip(0);
controllerGrip1.add(controllerModelFactory.createControllerModel(controllerGrip1));
this.scene.add(controllerGrip1);
let controllerGrip2 = this.renderer.xr.getControllerGrip( 1 );
controllerGrip2.add( controllerModelFactory.createControllerModel( controllerGrip2 ) );
this.scene.add( controllerGrip2 );
let controllerGrip2 = this.renderer.xr.getControllerGrip(1);
controllerGrip2.add(controllerModelFactory.createControllerModel(controllerGrip2));
this.scene.add(controllerGrip2);
const geometry = new THREE.BufferGeometry().setFromPoints( [ new THREE.Vector3( 0, 0, 0 ), new THREE.Vector3( 0, 0, - 1 ) ] );
const geometry = new THREE.BufferGeometry().setFromPoints([new THREE.Vector3(0, 0, 0), new THREE.Vector3(0, 0, - 1)]);
let line = new THREE.Line( geometry );
let line = new THREE.Line(geometry);
line.name = 'line';
line.scale.z = 5;
@@ -210,42 +217,84 @@ class GameEngine {
this.xrController2 = c2
}
handleXrAction(gameEngine, delta){
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){
if (gameEngine.xrController1?.gamepad) {
let gp = gameEngine.xrController1.gamepad;
if (gp.axes[3] != 0){
if (gp.axes[3] != 0) {
gameEngine.scene.position.z += gp.axes[3] * delta;
}
if (gp.axes[2] != 0){
if (gp.axes[2] != 0) {
gameEngine.scene.position.x += gp.axes[2] * delta;
}
}
if (gameEngine.xrController2?.gamepad){
if (gameEngine.xrController2?.gamepad) {
let gp = gameEngine.xrController2.gamepad;
let gp1 = gameEngine.xrController1.gamepad;
// if (gp.axes[3] != 0){
// let sc = gameEngine.scene.scale.x + gp.axes[3] * delta * 0.5;
// gameEngine.scene.scale.set(sc, sc, sc);
// }
if (gp.axes[2] != 0){
if (gp1.buttons[4]?.pressed){
if (gp.axes[2] != 0) {
if (gp1.buttons[4]?.pressed) {
gameEngine.scene.position.y += gp.axes[2] * delta;
}else if (gp1.buttons[5]?.pressed){
} else if (gp1.buttons[5]?.pressed) {
let sc = gameEngine.scene.scale.x * (gp.axes[2] * delta * 0.5 + 1);
gameEngine.scene.scale.set(sc, sc, sc);
}else{
} else {
gameEngine.scene.rotation.y += gp.axes[2] * delta * 0.5;
}
}
if (gp.buttons[4]?.pressed){
if (gp.buttons[4]?.pressed) {
// gameEngine.setCameraOrthographic();
// gameEngine.renderer.xr.updateCamera(gameEngine.orthographicCamera);
let session = gameEngine.renderer.xr.getFrame().session;
console.log(session);
session.pauseDepthSensing();
}
if (gp.buttons[5]?.pressed){
if (gp.buttons[5]?.pressed) {
// gameEngine.setCameraOrthographic();
// gameEngine.renderer.xr.updateCamera(gameEngine.orthographicCamera);
let session = gameEngine.renderer.xr.getFrame().session;
@@ -257,7 +306,7 @@ class GameEngine {
onControllerEvent(event) {
//this.handleXrAction(event, this);
event.type !=='move' && console.log(event)
event.type !== 'move' && console.log(event)
// const controller = event.target;
// if (controller.userData.active === false) return;
// this.raycaster.setFromXRController(controller);
@@ -300,10 +349,10 @@ class GameEngine {
$ = THREE;
async load(url, progress){
return new Promise((resolve, reject)=>{
this.loader.load(url, o=>{
o.scene.traverse(object=>{
async load(url, progress) {
return new Promise((resolve, reject) => {
this.loader.load(url, o => {
o.scene.traverse(object => {
// if (object.name == 'environment'){
// console.log('env recomputing')
// object.material.shading = THREE.SmoothShading;
@@ -318,9 +367,9 @@ class GameEngine {
})
}
async loadTexture(url, progress){
return new Promise((resolve, reject)=>{
new THREE.TextureLoader().load(url, texture=>{
async loadTexture(url, progress) {
return new Promise((resolve, reject) => {
new THREE.TextureLoader().load(url, texture => {
//texture.encoding = THREE.sRGBEncoding;
texture.colorSpace = THREE.SRGBColorSpace;
resolve(texture)
@@ -328,54 +377,54 @@ class GameEngine {
})
}
async loadPanorama(url){
async loadPanorama(url) {
let t = await this.loadTexture(url);
t.mapping = THREE.EquirectangularReflectionMapping;
this.scene.background = t;
this.scene.environment = t;
}
async captureScreenshot(type = 'image/webp', quality = 80){
return new Promise((resolve, reject)=>{
async captureScreenshot(type = 'image/webp', quality = 80) {
return new Promise((resolve, reject) => {
this.renderer.domElement.toBlob(resolve, type, quality)
})
}
playAnimation(object, clip, play = true){
let action = this.mixer.clipAction(clip, object);
playAnimation(object, clip, play = true) {
let action = this.mixers[0].clipAction(clip, object);
if (play) action.play();
else action.stop();
}
stop(){
stop() {
this.renderer?.setAnimationLoop(null);
}
getMouseVector(mouseEvent, domElement){
getMouseVector(mouseEvent, domElement) {
//console.log(mouseEvent, domElement)
const mouse = new THREE.Vector2();
if (this.renderType == 'VR'){
if (this.renderType == 'VR') {
let x;
if (mouseEvent.offsetX > window.innerWidth / 2){
if (mouseEvent.offsetX > window.innerWidth / 2) {
x = (mouseEvent.offsetX - window.innerWidth / 2) * 2
}else {
} else {
x = mouseEvent.offsetX * 2;
}
mouse.x = ( x / window.innerWidth ) * 2 - 1;
mouse.y = - ( mouseEvent.offsetY / window.innerHeight ) * 2 + 1;
}else{
mouse.x = ( mouseEvent.offsetX / domElement.clientWidth ) * 2 - 1;
mouse.y = - ( mouseEvent.offsetY / domElement.clientHeight ) * 2 + 1;
mouse.x = (x / window.innerWidth) * 2 - 1;
mouse.y = - (mouseEvent.offsetY / window.innerHeight) * 2 + 1;
} else {
mouse.x = (mouseEvent.offsetX / domElement.clientWidth) * 2 - 1;
mouse.y = - (mouseEvent.offsetY / domElement.clientHeight) * 2 + 1;
}
return mouse;
}
intersect(mouseEvent, domElement, objects, recursive = false, returnInputObjects = true){
intersect(mouseEvent, domElement, objects, recursive = false, returnInputObjects = true) {
let mouse = this.getMouseVector(mouseEvent, domElement);
this.raycaster.setFromCamera(mouse, this.camera);
let intersects = this.raycaster.intersectObjects(objects.filter(o=>o.visible), recursive);
if (returnInputObjects && recursive){
intersects.forEach(o=>{
let intersects = this.raycaster.intersectObjects(objects.filter(o => o.visible), recursive);
if (returnInputObjects && recursive) {
intersects.forEach(o => {
while (o.object && !objects.includes(o.object)) {
o.object = o.object.parent;
}
@@ -384,13 +433,13 @@ class GameEngine {
return intersects;
}
autoScale(object, mk = 1){
autoScale(object, mk = 1) {
let bb = new THREE.Box3().setFromObject(object);
let k = Math.max(bb.max.x - bb.min.x, bb.max.y - bb.min.y, bb.max.z - bb.min.z);
object.scale.multiplyScalar(mk/k);
object.scale.multiplyScalar(mk / k);
}
setCamera(camera){
setCamera(camera) {
//camera.updateProjectionMatrix();
this.camera = camera;
//this.transformControls.camera = camera;
@@ -413,11 +462,11 @@ class GameEngine {
this.camera = o;
this.transformControls.camera = o;
this.orbitControls.object = o;
if (this.gizmo){
if (this.gizmo) {
this.gizmo.camera = o;
}
}
setCameraPerspective() {
let o = this.perspectiveCamera;
// const oldY = o.position.y;
@@ -427,12 +476,12 @@ class GameEngine {
this.camera = o;
this.transformControls.camera = o;
this.orbitControls.object = o;
if (this.gizmo){
if (this.gizmo) {
this.gizmo.camera = o;
}
}
resize(w, h){
resize(w, h) {
this.w = w;
this.h = h;
this.aspect = this.w / this.h
@@ -443,21 +492,21 @@ class GameEngine {
this.orthographicCamera.top = this.frustumSize / 2;
this.orthographicCamera.bottom = - this.frustumSize / 2;
this.renderer.setSize(w, h);
this.renderer.setViewport( 0, 0, this.w, this.h );
this.anaglyph.setSize( w, h );
this.stereo.setSize( w, h );
this.renderer.setViewport(0, 0, this.w, this.h);
this.anaglyph.setSize(w, h);
this.stereo.setSize(w, h);
this.gizmo?.update();
}
render(scene, camera){
if (this.renderType == 'VR'){
this.stereo?.render( scene, camera );
}else if (this.renderType == 'AG'){
this.anaglyph?.render( scene, camera );
}else{
this.renderer.render( scene, camera );
render(scene, camera) {
if (this.renderType == 'VR') {
this.stereo?.render(scene, camera);
} else if (this.renderType == 'AG') {
this.anaglyph?.render(scene, camera);
} else {
this.renderer.render(scene, camera);
}
}
}
export {GameEngine}
export { GameEngine }