refactor
This commit is contained in:
22
app/node_modules/.vite/deps/_metadata.json
generated
vendored
22
app/node_modules/.vite/deps/_metadata.json
generated
vendored
@@ -1,25 +1,25 @@
|
|||||||
{
|
{
|
||||||
"hash": "fe2b1553",
|
"hash": "4305885f",
|
||||||
"configHash": "cba10d12",
|
"configHash": "3b37792f",
|
||||||
"lockfileHash": "0d3c4966",
|
"lockfileHash": "0d3c4966",
|
||||||
"browserHash": "5cb401b4",
|
"browserHash": "b77d7025",
|
||||||
"optimized": {
|
"optimized": {
|
||||||
"three": {
|
|
||||||
"src": "../../three/build/three.module.js",
|
|
||||||
"file": "three.js",
|
|
||||||
"fileHash": "3c7c8ebb",
|
|
||||||
"needsInterop": false
|
|
||||||
},
|
|
||||||
"cannon-es": {
|
"cannon-es": {
|
||||||
"src": "../../cannon-es/dist/cannon-es.js",
|
"src": "../../cannon-es/dist/cannon-es.js",
|
||||||
"file": "cannon-es.js",
|
"file": "cannon-es.js",
|
||||||
"fileHash": "8efd3187",
|
"fileHash": "55d87002",
|
||||||
|
"needsInterop": false
|
||||||
|
},
|
||||||
|
"three": {
|
||||||
|
"src": "../../three/build/three.module.js",
|
||||||
|
"file": "three.js",
|
||||||
|
"fileHash": "c1c85acc",
|
||||||
"needsInterop": false
|
"needsInterop": false
|
||||||
},
|
},
|
||||||
"three/examples/jsm/loaders/GLTFLoader.js": {
|
"three/examples/jsm/loaders/GLTFLoader.js": {
|
||||||
"src": "../../three/examples/jsm/loaders/GLTFLoader.js",
|
"src": "../../three/examples/jsm/loaders/GLTFLoader.js",
|
||||||
"file": "three_examples_jsm_loaders_GLTFLoader__js.js",
|
"file": "three_examples_jsm_loaders_GLTFLoader__js.js",
|
||||||
"fileHash": "6222a00f",
|
"fileHash": "4e20fb05",
|
||||||
"needsInterop": false
|
"needsInterop": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
99
app/src/README.md
Normal file
99
app/src/README.md
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
# AAF Systems Homepage - Code Structure
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
The main.ts file has been refactored into a modular structure for better maintainability and organization.
|
||||||
|
|
||||||
|
## File Structure
|
||||||
|
|
||||||
|
### `src/main.ts` (133 lines → from 678 lines)
|
||||||
|
**Main application class and initialization**
|
||||||
|
- `AAFHomepage` class - Main application orchestrator
|
||||||
|
- Scene, camera, and renderer setup
|
||||||
|
- Animation loop coordination
|
||||||
|
- Application initialization
|
||||||
|
|
||||||
|
### `src/types.ts` (11 lines)
|
||||||
|
**Type definitions and interfaces**
|
||||||
|
- `PhysicsObject` interface - Defines mesh and physics body pairing
|
||||||
|
- `ModelGeometry` interface - Defines extracted model geometry data structure
|
||||||
|
|
||||||
|
### `src/lighting.ts` (42 lines)
|
||||||
|
**Lighting management**
|
||||||
|
- `LightingManager` class with static methods
|
||||||
|
- Studio lighting setup with multiple light types:
|
||||||
|
- Ambient light for general illumination
|
||||||
|
- Directional lights (key, fill, rim)
|
||||||
|
- Point lights for atmosphere
|
||||||
|
|
||||||
|
### `src/model-loader.ts` (311 lines)
|
||||||
|
**3D model loading and processing**
|
||||||
|
- `ModelLoader` class - Handles GLTF model operations
|
||||||
|
- Geometry extraction and processing
|
||||||
|
- Geometry downsampling for performance optimization
|
||||||
|
- Collision shape generation for physics
|
||||||
|
- Fallback object creation when model loading fails
|
||||||
|
- Model instancing with physics bodies
|
||||||
|
|
||||||
|
### `src/physics.ts` (98 lines)
|
||||||
|
**Physics simulation management**
|
||||||
|
- `PhysicsManager` class with static methods
|
||||||
|
- Physics world configuration and setup
|
||||||
|
- Force application (attraction, repulsion, damping)
|
||||||
|
- Physics stepping and object synchronization
|
||||||
|
- Mouse interaction physics
|
||||||
|
|
||||||
|
### `src/event-manager.ts` (40 lines)
|
||||||
|
**Event handling and user input**
|
||||||
|
- `EventManager` class - Manages user interactions
|
||||||
|
- Mouse movement tracking and world position calculation
|
||||||
|
- Window resize handling
|
||||||
|
- Raycasting for mouse interaction
|
||||||
|
|
||||||
|
## Benefits of This Structure
|
||||||
|
|
||||||
|
### 🔧 **Maintainability**
|
||||||
|
- Each file has a single responsibility
|
||||||
|
- Easier to locate and modify specific functionality
|
||||||
|
- Reduced file size makes navigation simpler
|
||||||
|
|
||||||
|
### 🔄 **Reusability**
|
||||||
|
- Modules can be easily reused in other projects
|
||||||
|
- Static utility classes provide clean interfaces
|
||||||
|
- Type definitions are centralized and shared
|
||||||
|
|
||||||
|
### 🧪 **Testability**
|
||||||
|
- Individual modules can be unit tested in isolation
|
||||||
|
- Dependencies are clearly defined through imports
|
||||||
|
- Mock objects can be easily substituted
|
||||||
|
|
||||||
|
### 👥 **Team Collaboration**
|
||||||
|
- Multiple developers can work on different aspects simultaneously
|
||||||
|
- Clear separation of concerns reduces merge conflicts
|
||||||
|
- Code reviews are more focused and manageable
|
||||||
|
|
||||||
|
### 📦 **Performance**
|
||||||
|
- Tree-shaking can remove unused code more effectively
|
||||||
|
- Smaller bundles through modular imports
|
||||||
|
- Better browser caching of individual modules
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
The refactored code maintains the same external API. Simply import and use:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import app from './main'
|
||||||
|
// The application initializes automatically
|
||||||
|
```
|
||||||
|
|
||||||
|
## Module Dependencies
|
||||||
|
|
||||||
|
```
|
||||||
|
main.ts
|
||||||
|
├── types.ts
|
||||||
|
├── lighting.ts
|
||||||
|
├── model-loader.ts (depends on types.ts)
|
||||||
|
├── physics.ts
|
||||||
|
└── event-manager.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
All modules are designed to be stateless utilities or encapsulated classes, promoting clean architecture principles.
|
||||||
52
app/src/event-manager.ts
Normal file
52
app/src/event-manager.ts
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import * as THREE from 'three'
|
||||||
|
|
||||||
|
export class EventManager {
|
||||||
|
private mouse: THREE.Vector2
|
||||||
|
private raycaster: THREE.Raycaster
|
||||||
|
private mouseWorldPosition: THREE.Vector3
|
||||||
|
private camera: THREE.PerspectiveCamera
|
||||||
|
private renderer: THREE.WebGLRenderer
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
mouse: THREE.Vector2,
|
||||||
|
raycaster: THREE.Raycaster,
|
||||||
|
mouseWorldPosition: THREE.Vector3,
|
||||||
|
camera: THREE.PerspectiveCamera,
|
||||||
|
renderer: THREE.WebGLRenderer
|
||||||
|
) {
|
||||||
|
this.mouse = mouse
|
||||||
|
this.raycaster = raycaster
|
||||||
|
this.mouseWorldPosition = mouseWorldPosition
|
||||||
|
this.camera = camera
|
||||||
|
this.renderer = renderer
|
||||||
|
}
|
||||||
|
|
||||||
|
setupEventListeners(): void {
|
||||||
|
// Mouse move event
|
||||||
|
window.addEventListener('mousemove', (event) => {
|
||||||
|
this.mouse.x = (event.clientX / window.innerWidth) * 2 - 1
|
||||||
|
this.mouse.y = -(event.clientY / window.innerHeight) * 2 + 1
|
||||||
|
|
||||||
|
// Update raycaster
|
||||||
|
this.raycaster.setFromCamera(this.mouse, this.camera)
|
||||||
|
|
||||||
|
// Calculate mouse position in world space
|
||||||
|
const distance = 10 // Distance from camera
|
||||||
|
const direction = this.raycaster.ray.direction.clone()
|
||||||
|
direction.multiplyScalar(distance)
|
||||||
|
|
||||||
|
// Update the mouse world position by setting its components directly
|
||||||
|
this.mouseWorldPosition.copy(this.camera.position).add(direction)
|
||||||
|
|
||||||
|
// Debug log to verify mouse interaction is working
|
||||||
|
console.log('Mouse world position:', this.mouseWorldPosition.x.toFixed(2), this.mouseWorldPosition.y.toFixed(2), this.mouseWorldPosition.z.toFixed(2))
|
||||||
|
})
|
||||||
|
|
||||||
|
// Window resize event
|
||||||
|
window.addEventListener('resize', () => {
|
||||||
|
this.camera.aspect = window.innerWidth / window.innerHeight
|
||||||
|
this.camera.updateProjectionMatrix()
|
||||||
|
this.renderer.setSize(window.innerWidth, window.innerHeight)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
42
app/src/lighting.ts
Normal file
42
app/src/lighting.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import * as THREE from 'three'
|
||||||
|
|
||||||
|
export class LightingManager {
|
||||||
|
static setupLighting(scene: THREE.Scene): void {
|
||||||
|
// Ambient light for general illumination
|
||||||
|
const ambientLight = new THREE.AmbientLight(0x404040, 0.3)
|
||||||
|
scene.add(ambientLight)
|
||||||
|
|
||||||
|
// Key light (main studio light)
|
||||||
|
const keyLight = new THREE.DirectionalLight(0xffffff, 1.2)
|
||||||
|
keyLight.position.set(10, 10, 5)
|
||||||
|
keyLight.castShadow = true
|
||||||
|
keyLight.shadow.mapSize.width = 2048
|
||||||
|
keyLight.shadow.mapSize.height = 2048
|
||||||
|
keyLight.shadow.camera.near = 0.1
|
||||||
|
keyLight.shadow.camera.far = 50
|
||||||
|
keyLight.shadow.camera.left = -10
|
||||||
|
keyLight.shadow.camera.right = 10
|
||||||
|
keyLight.shadow.camera.top = 10
|
||||||
|
keyLight.shadow.camera.bottom = -10
|
||||||
|
scene.add(keyLight)
|
||||||
|
|
||||||
|
// Fill light (softer, opposite side)
|
||||||
|
const fillLight = new THREE.DirectionalLight(0x8888ff, 0.4)
|
||||||
|
fillLight.position.set(-8, 5, 3)
|
||||||
|
scene.add(fillLight)
|
||||||
|
|
||||||
|
// Rim light (for edge definition)
|
||||||
|
const rimLight = new THREE.DirectionalLight(0xffaa88, 0.6)
|
||||||
|
rimLight.position.set(0, -10, -5)
|
||||||
|
scene.add(rimLight)
|
||||||
|
|
||||||
|
// Point lights for additional atmosphere
|
||||||
|
const pointLight1 = new THREE.PointLight(0x4444ff, 0.5, 30)
|
||||||
|
pointLight1.position.set(-5, 5, 10)
|
||||||
|
scene.add(pointLight1)
|
||||||
|
|
||||||
|
const pointLight2 = new THREE.PointLight(0xff4444, 0.3, 25)
|
||||||
|
pointLight2.position.set(5, -5, -8)
|
||||||
|
scene.add(pointLight2)
|
||||||
|
}
|
||||||
|
}
|
||||||
600
app/src/main.ts
600
app/src/main.ts
@@ -1,18 +1,11 @@
|
|||||||
import './style.css'
|
import './style.css'
|
||||||
import * as THREE from 'three'
|
import * as THREE from 'three'
|
||||||
import * as CANNON from 'cannon-es'
|
import * as CANNON from 'cannon-es'
|
||||||
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'
|
import type { PhysicsObject } from './types'
|
||||||
|
import { LightingManager } from './lighting'
|
||||||
interface PhysicsObject {
|
import { ModelLoader } from './model-loader'
|
||||||
mesh: THREE.Object3D
|
import { PhysicsManager } from './physics'
|
||||||
body: CANNON.Body
|
import { EventManager } from './event-manager'
|
||||||
}
|
|
||||||
|
|
||||||
interface ModelGeometry {
|
|
||||||
vertices: number[]
|
|
||||||
indices: number[]
|
|
||||||
boundingBox: THREE.Box3
|
|
||||||
}
|
|
||||||
|
|
||||||
class AAFHomepage {
|
class AAFHomepage {
|
||||||
private scene: THREE.Scene
|
private scene: THREE.Scene
|
||||||
@@ -23,10 +16,10 @@ class AAFHomepage {
|
|||||||
private mouse: THREE.Vector2
|
private mouse: THREE.Vector2
|
||||||
private raycaster: THREE.Raycaster
|
private raycaster: THREE.Raycaster
|
||||||
private attractionPoint: THREE.Vector3
|
private attractionPoint: THREE.Vector3
|
||||||
private loader: GLTFLoader
|
|
||||||
private mouseWorldPosition: THREE.Vector3
|
private mouseWorldPosition: THREE.Vector3
|
||||||
private lastTime: number = 0
|
private lastTime: number = 0
|
||||||
private modelGeometry: ModelGeometry | null = null
|
private modelLoader: ModelLoader
|
||||||
|
private eventManager: EventManager
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
console.log('AAFHomepage constructor started')
|
console.log('AAFHomepage constructor started')
|
||||||
@@ -47,12 +40,21 @@ class AAFHomepage {
|
|||||||
antialias: true,
|
antialias: true,
|
||||||
alpha: true
|
alpha: true
|
||||||
})
|
})
|
||||||
this.world = new CANNON.World()
|
this.world = PhysicsManager.setupPhysicsWorld()
|
||||||
this.mouse = new THREE.Vector2()
|
this.mouse = new THREE.Vector2()
|
||||||
this.raycaster = new THREE.Raycaster()
|
this.raycaster = new THREE.Raycaster()
|
||||||
this.attractionPoint = new THREE.Vector3(0, 0, 0)
|
this.attractionPoint = new THREE.Vector3(0, 0, 0)
|
||||||
this.loader = new GLTFLoader()
|
|
||||||
this.mouseWorldPosition = new THREE.Vector3()
|
this.mouseWorldPosition = new THREE.Vector3()
|
||||||
|
this.modelLoader = new ModelLoader()
|
||||||
|
|
||||||
|
// Initialize event manager
|
||||||
|
this.eventManager = new EventManager(
|
||||||
|
this.mouse,
|
||||||
|
this.raycaster,
|
||||||
|
this.mouseWorldPosition,
|
||||||
|
this.camera,
|
||||||
|
this.renderer
|
||||||
|
)
|
||||||
|
|
||||||
// Initialize the scene
|
// Initialize the scene
|
||||||
this.init()
|
this.init()
|
||||||
@@ -78,32 +80,10 @@ class AAFHomepage {
|
|||||||
|
|
||||||
console.log('Camera positioned')
|
console.log('Camera positioned')
|
||||||
|
|
||||||
// Setup physics world
|
|
||||||
this.world.gravity.set(0, 0, 0)
|
|
||||||
|
|
||||||
// Use SAPBroadphase for better collision detection with many objects
|
|
||||||
this.world.broadphase = new CANNON.SAPBroadphase(this.world)
|
|
||||||
|
|
||||||
// Enable collision detection and response
|
|
||||||
this.world.allowSleep = false // Prevent objects from sleeping
|
|
||||||
|
|
||||||
// Configure contact material for better collisions
|
|
||||||
const defaultMaterial = new CANNON.Material('default')
|
|
||||||
const defaultContactMaterial = new CANNON.ContactMaterial(defaultMaterial, defaultMaterial, {
|
|
||||||
friction: 0.4,
|
|
||||||
restitution: 0.3,
|
|
||||||
contactEquationStiffness: 1e8,
|
|
||||||
contactEquationRelaxation: 3,
|
|
||||||
frictionEquationStiffness: 1e8,
|
|
||||||
frictionEquationRelaxation: 3
|
|
||||||
})
|
|
||||||
this.world.addContactMaterial(defaultContactMaterial)
|
|
||||||
this.world.defaultMaterial = defaultMaterial
|
|
||||||
|
|
||||||
console.log('Physics world setup')
|
console.log('Physics world setup')
|
||||||
|
|
||||||
// Setup lighting
|
// Setup lighting
|
||||||
this.setupLighting()
|
LightingManager.setupLighting(this.scene)
|
||||||
|
|
||||||
console.log('Lighting setup complete')
|
console.log('Lighting setup complete')
|
||||||
|
|
||||||
@@ -113,7 +93,7 @@ class AAFHomepage {
|
|||||||
console.log('Model loading initiated')
|
console.log('Model loading initiated')
|
||||||
|
|
||||||
// Setup event listeners
|
// Setup event listeners
|
||||||
this.setupEventListeners()
|
this.eventManager.setupEventListeners()
|
||||||
|
|
||||||
console.log('Event listeners setup')
|
console.log('Event listeners setup')
|
||||||
|
|
||||||
@@ -123,537 +103,8 @@ class AAFHomepage {
|
|||||||
console.log('Animation loop started')
|
console.log('Animation loop started')
|
||||||
}
|
}
|
||||||
|
|
||||||
private setupLighting(): void {
|
|
||||||
// Ambient light for general illumination
|
|
||||||
const ambientLight = new THREE.AmbientLight(0x404040, 0.3)
|
|
||||||
this.scene.add(ambientLight)
|
|
||||||
|
|
||||||
// Key light (main studio light)
|
|
||||||
const keyLight = new THREE.DirectionalLight(0xffffff, 1.2)
|
|
||||||
keyLight.position.set(10, 10, 5)
|
|
||||||
keyLight.castShadow = true
|
|
||||||
keyLight.shadow.mapSize.width = 2048
|
|
||||||
keyLight.shadow.mapSize.height = 2048
|
|
||||||
keyLight.shadow.camera.near = 0.1
|
|
||||||
keyLight.shadow.camera.far = 50
|
|
||||||
keyLight.shadow.camera.left = -10
|
|
||||||
keyLight.shadow.camera.right = 10
|
|
||||||
keyLight.shadow.camera.top = 10
|
|
||||||
keyLight.shadow.camera.bottom = -10
|
|
||||||
this.scene.add(keyLight)
|
|
||||||
|
|
||||||
// Fill light (softer, opposite side)
|
|
||||||
const fillLight = new THREE.DirectionalLight(0x8888ff, 0.4)
|
|
||||||
fillLight.position.set(-8, 5, 3)
|
|
||||||
this.scene.add(fillLight)
|
|
||||||
|
|
||||||
// Rim light (for edge definition)
|
|
||||||
const rimLight = new THREE.DirectionalLight(0xffaa88, 0.6)
|
|
||||||
rimLight.position.set(0, -10, -5)
|
|
||||||
this.scene.add(rimLight)
|
|
||||||
|
|
||||||
// Point lights for additional atmosphere
|
|
||||||
const pointLight1 = new THREE.PointLight(0x4444ff, 0.5, 30)
|
|
||||||
pointLight1.position.set(-5, 5, 10)
|
|
||||||
this.scene.add(pointLight1)
|
|
||||||
|
|
||||||
const pointLight2 = new THREE.PointLight(0xff4444, 0.3, 25)
|
|
||||||
pointLight2.position.set(5, -5, -8)
|
|
||||||
this.scene.add(pointLight2)
|
|
||||||
}
|
|
||||||
|
|
||||||
private extractModelGeometry(gltf: any): ModelGeometry | null {
|
|
||||||
const geometries: THREE.BufferGeometry[] = []
|
|
||||||
const boundingBox = new THREE.Box3()
|
|
||||||
|
|
||||||
// Update world matrices first
|
|
||||||
gltf.scene.updateMatrixWorld(true)
|
|
||||||
|
|
||||||
// Traverse the model to find all mesh geometries
|
|
||||||
gltf.scene.traverse((child: any) => {
|
|
||||||
if (child instanceof THREE.Mesh && child.geometry) {
|
|
||||||
// Apply the mesh's transform to the geometry
|
|
||||||
const geometry = child.geometry.clone() as THREE.BufferGeometry
|
|
||||||
|
|
||||||
// Only apply matrix if it's not identity
|
|
||||||
if (!child.matrixWorld.equals(new THREE.Matrix4())) {
|
|
||||||
geometry.applyMatrix4(child.matrixWorld)
|
|
||||||
}
|
|
||||||
|
|
||||||
geometries.push(geometry)
|
|
||||||
|
|
||||||
// Update bounding box
|
|
||||||
const positionAttr = geometry.getAttribute('position') as THREE.BufferAttribute
|
|
||||||
if (positionAttr) {
|
|
||||||
const geometryBoundingBox = new THREE.Box3().setFromBufferAttribute(positionAttr)
|
|
||||||
boundingBox.union(geometryBoundingBox)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
if (geometries.length === 0) {
|
|
||||||
console.warn('No geometries found in model')
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`Found ${geometries.length} geometries to merge`)
|
|
||||||
|
|
||||||
// Merge all geometries
|
|
||||||
const combinedGeometry = this.mergeBufferGeometries(geometries)
|
|
||||||
|
|
||||||
// Extract vertices and faces
|
|
||||||
const positionAttribute = combinedGeometry.getAttribute('position') as THREE.BufferAttribute
|
|
||||||
if (!positionAttribute) {
|
|
||||||
console.error('No position attribute found in combined geometry')
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
const vertices: number[] = Array.from(positionAttribute.array as Float32Array)
|
|
||||||
|
|
||||||
let indices: number[] = []
|
|
||||||
if (combinedGeometry.index) {
|
|
||||||
indices = Array.from(combinedGeometry.index.array)
|
|
||||||
} else {
|
|
||||||
// Generate indices if none exist
|
|
||||||
for (let i = 0; i < vertices.length / 3; i++) {
|
|
||||||
indices.push(i)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`Extracted ${vertices.length / 3} vertices and ${indices.length / 3} triangles`)
|
|
||||||
|
|
||||||
combinedGeometry.dispose()
|
|
||||||
|
|
||||||
return {
|
|
||||||
vertices,
|
|
||||||
indices,
|
|
||||||
boundingBox
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private mergeBufferGeometries(geometries: THREE.BufferGeometry[]): THREE.BufferGeometry {
|
|
||||||
const merged = new THREE.BufferGeometry()
|
|
||||||
const attributes: { [key: string]: THREE.BufferAttribute[] } = {}
|
|
||||||
let indexOffset = 0
|
|
||||||
const mergedIndices: number[] = []
|
|
||||||
|
|
||||||
// Collect all attributes
|
|
||||||
geometries.forEach(geometry => {
|
|
||||||
const attributeNames = Object.keys(geometry.attributes)
|
|
||||||
attributeNames.forEach(name => {
|
|
||||||
if (!attributes[name]) attributes[name] = []
|
|
||||||
const attr = geometry.attributes[name]
|
|
||||||
if (attr instanceof THREE.BufferAttribute) {
|
|
||||||
attributes[name].push(attr)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Handle indices
|
|
||||||
if (geometry.index) {
|
|
||||||
const indices = Array.from(geometry.index.array)
|
|
||||||
indices.forEach(index => mergedIndices.push(index + indexOffset))
|
|
||||||
indexOffset += geometry.attributes.position.count
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Merge attributes
|
|
||||||
Object.keys(attributes).forEach(name => {
|
|
||||||
const attributeArrays = attributes[name]
|
|
||||||
const itemSize = attributeArrays[0].itemSize
|
|
||||||
const normalized = attributeArrays[0].normalized
|
|
||||||
|
|
||||||
let totalCount = 0
|
|
||||||
attributeArrays.forEach(attr => totalCount += attr.count)
|
|
||||||
|
|
||||||
const mergedArray = new Float32Array(totalCount * itemSize)
|
|
||||||
let offset = 0
|
|
||||||
|
|
||||||
attributeArrays.forEach(attr => {
|
|
||||||
mergedArray.set(attr.array as Float32Array, offset)
|
|
||||||
offset += attr.array.length
|
|
||||||
})
|
|
||||||
|
|
||||||
merged.setAttribute(name, new THREE.BufferAttribute(mergedArray, itemSize, normalized))
|
|
||||||
})
|
|
||||||
|
|
||||||
if (mergedIndices.length > 0) {
|
|
||||||
merged.setIndex(mergedIndices)
|
|
||||||
}
|
|
||||||
|
|
||||||
return merged
|
|
||||||
}
|
|
||||||
|
|
||||||
private downsampleGeometry(geometry: ModelGeometry, factor: number = 0.3): ModelGeometry {
|
|
||||||
// Simple downsampling by taking every Nth vertex
|
|
||||||
const step = Math.max(1, Math.floor(1 / factor))
|
|
||||||
const downsampledVertices: number[] = []
|
|
||||||
const downsampledIndices: number[] = []
|
|
||||||
const vertexMap = new Map<number, number>()
|
|
||||||
|
|
||||||
// Downsample vertices
|
|
||||||
for (let i = 0; i < geometry.vertices.length; i += step * 3) {
|
|
||||||
if (i + 2 < geometry.vertices.length) {
|
|
||||||
const originalIndex = i / 3
|
|
||||||
const newIndex = downsampledVertices.length / 3
|
|
||||||
vertexMap.set(originalIndex, newIndex)
|
|
||||||
|
|
||||||
downsampledVertices.push(
|
|
||||||
geometry.vertices[i],
|
|
||||||
geometry.vertices[i + 1],
|
|
||||||
geometry.vertices[i + 2]
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Downsample indices, keeping only triangles where all vertices are kept
|
|
||||||
for (let i = 0; i < geometry.indices.length; i += 3) {
|
|
||||||
const v1 = geometry.indices[i]
|
|
||||||
const v2 = geometry.indices[i + 1]
|
|
||||||
const v3 = geometry.indices[i + 2]
|
|
||||||
|
|
||||||
if (vertexMap.has(v1) && vertexMap.has(v2) && vertexMap.has(v3)) {
|
|
||||||
downsampledIndices.push(
|
|
||||||
vertexMap.get(v1)!,
|
|
||||||
vertexMap.get(v2)!,
|
|
||||||
vertexMap.get(v3)!
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
vertices: downsampledVertices,
|
|
||||||
indices: downsampledIndices,
|
|
||||||
boundingBox: geometry.boundingBox
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private createCollisionShape(geometry: ModelGeometry, scale: number = 1): CANNON.Shape {
|
|
||||||
try {
|
|
||||||
// For performance, use a simplified approach
|
|
||||||
// If the geometry is too complex, fall back to a hull or sphere
|
|
||||||
const vertexCount = geometry.vertices.length / 3
|
|
||||||
|
|
||||||
if (vertexCount > 100) {
|
|
||||||
// For complex meshes, use bounding box approximation
|
|
||||||
const size = geometry.boundingBox.getSize(new THREE.Vector3())
|
|
||||||
const scaledSize = size.multiplyScalar(scale)
|
|
||||||
return new CANNON.Box(new CANNON.Vec3(
|
|
||||||
scaledSize.x * 0.5,
|
|
||||||
scaledSize.y * 0.5,
|
|
||||||
scaledSize.z * 0.5
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
// For simpler meshes, try convex hull
|
|
||||||
const scaledVertices = geometry.vertices.map(v => v * scale)
|
|
||||||
const vertices: CANNON.Vec3[] = []
|
|
||||||
|
|
||||||
for (let i = 0; i < scaledVertices.length; i += 3) {
|
|
||||||
vertices.push(new CANNON.Vec3(
|
|
||||||
scaledVertices[i],
|
|
||||||
scaledVertices[i + 1],
|
|
||||||
scaledVertices[i + 2]
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Limit faces to prevent performance issues
|
|
||||||
const faces: number[][] = []
|
|
||||||
const maxFaces = Math.min(geometry.indices.length / 3, 50)
|
|
||||||
|
|
||||||
for (let i = 0; i < maxFaces * 3; i += 3) {
|
|
||||||
const v1 = geometry.indices[i]
|
|
||||||
const v2 = geometry.indices[i + 1]
|
|
||||||
const v3 = geometry.indices[i + 2]
|
|
||||||
|
|
||||||
if (v1 < vertices.length && v2 < vertices.length && v3 < vertices.length) {
|
|
||||||
faces.push([v1, v2, v3])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (faces.length > 0) {
|
|
||||||
const shape = new CANNON.ConvexPolyhedron({ vertices, faces })
|
|
||||||
return shape
|
|
||||||
} else {
|
|
||||||
throw new Error('No valid faces')
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.warn('Failed to create complex collision shape, falling back to sphere:', error)
|
|
||||||
// Fallback to sphere using bounding box
|
|
||||||
const size = geometry.boundingBox.getSize(new THREE.Vector3())
|
|
||||||
const radius = Math.max(size.x, size.y, size.z) * 0.5 * scale
|
|
||||||
return new CANNON.Sphere(Math.max(0.3, radius))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async loadAndCreateObjects(): Promise<void> {
|
private async loadAndCreateObjects(): Promise<void> {
|
||||||
try {
|
await this.modelLoader.loadAndCreateObjects(this.scene, this.world, this.physicsObjects)
|
||||||
console.log('Loading main_model.glb...')
|
|
||||||
const gltf = await this.loader.loadAsync('/models/main_model.glb')
|
|
||||||
console.log('Model loaded successfully:', gltf)
|
|
||||||
|
|
||||||
// Extract and downsample the geometry for collision detection
|
|
||||||
const originalGeometry = this.extractModelGeometry(gltf)
|
|
||||||
if (originalGeometry) {
|
|
||||||
console.log('Extracted geometry with', originalGeometry.vertices.length / 3, 'vertices')
|
|
||||||
this.modelGeometry = this.downsampleGeometry(originalGeometry, 0.3) // 30% of original detail
|
|
||||||
console.log('Downsampled to', this.modelGeometry.vertices.length / 3, 'vertices for collision')
|
|
||||||
} else {
|
|
||||||
console.warn('Failed to extract geometry, will use simple collision shapes')
|
|
||||||
this.modelGeometry = null
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create multiple instances of the loaded model
|
|
||||||
const numInstances = 20
|
|
||||||
|
|
||||||
gltf.scene.traverse((child) => {
|
|
||||||
if (child instanceof THREE.Mesh) {
|
|
||||||
child.castShadow = true
|
|
||||||
child.receiveShadow = true
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
for (let i = 0; i < numInstances; i++) {
|
|
||||||
// Clone the model
|
|
||||||
const modelClone = gltf.scene.clone()
|
|
||||||
|
|
||||||
// Start objects close to the attraction point for immediate clustering
|
|
||||||
const x = (Math.random() - 0.5) * 6 // Reduced from 25 to 6
|
|
||||||
const y = (Math.random() - 0.5) * 6 // Reduced from 25 to 6
|
|
||||||
const z = (Math.random() - 0.5) * 6 // Reduced from 25 to 6
|
|
||||||
modelClone.position.set(x, y, z)
|
|
||||||
|
|
||||||
// Random scale variation (40% of current size)
|
|
||||||
const scale = (0.5 + Math.random() * 0.5) // Scaled down to 40%
|
|
||||||
modelClone.scale.setScalar(scale)
|
|
||||||
|
|
||||||
// Create physics body with custom collision shape or fallback to sphere
|
|
||||||
let shape: CANNON.Shape
|
|
||||||
if (this.modelGeometry) {
|
|
||||||
shape = this.createCollisionShape(this.modelGeometry, scale)
|
|
||||||
} else {
|
|
||||||
// Fallback to sphere if geometry extraction failed
|
|
||||||
const radius = Math.max(0.3, 0.8 * scale)
|
|
||||||
shape = new CANNON.Sphere(radius)
|
|
||||||
}
|
|
||||||
|
|
||||||
const body = new CANNON.Body({
|
|
||||||
mass: 1,
|
|
||||||
material: this.world.defaultMaterial,
|
|
||||||
type: CANNON.Body.DYNAMIC
|
|
||||||
})
|
|
||||||
body.addShape(shape)
|
|
||||||
body.position.set(x, y, z)
|
|
||||||
|
|
||||||
// Enable rotational dynamics
|
|
||||||
body.angularDamping = 0.1 // Small damping for realistic rotation
|
|
||||||
body.linearDamping = 0.05 // Small linear damping
|
|
||||||
|
|
||||||
// Prevent objects from sleeping during collisions
|
|
||||||
body.sleepSpeedLimit = 0.1
|
|
||||||
body.sleepTimeLimit = 1
|
|
||||||
|
|
||||||
// Add collision event listener with rotational effects
|
|
||||||
body.addEventListener('collide', (event: any) => {
|
|
||||||
console.log('Collision detected!')
|
|
||||||
|
|
||||||
// Add some spin on collision for more dynamic movement
|
|
||||||
const impactStrength = event.contact?.getImpactVelocityAlongNormal() || 1
|
|
||||||
const spinForce = Math.min(impactStrength * 0.5, 2) // Cap the spin force
|
|
||||||
|
|
||||||
body.angularVelocity.set(
|
|
||||||
body.angularVelocity.x + (Math.random() - 0.5) * spinForce,
|
|
||||||
body.angularVelocity.y + (Math.random() - 0.5) * spinForce,
|
|
||||||
body.angularVelocity.z + (Math.random() - 0.5) * spinForce
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
// Add gentle initial velocity and rotation
|
|
||||||
body.velocity.set(
|
|
||||||
(Math.random() - 0.5) * 1, // Reduced from 3 to 1
|
|
||||||
(Math.random() - 0.5) * 1, // Reduced from 3 to 1
|
|
||||||
(Math.random() - 0.5) * 1 // Reduced from 3 to 1
|
|
||||||
)
|
|
||||||
|
|
||||||
// Add initial angular velocity for natural rotation
|
|
||||||
body.angularVelocity.set(
|
|
||||||
(Math.random() - 0.5) * 2,
|
|
||||||
(Math.random() - 0.5) * 2,
|
|
||||||
(Math.random() - 0.5) * 2
|
|
||||||
)
|
|
||||||
|
|
||||||
this.scene.add(modelClone)
|
|
||||||
this.world.addBody(body)
|
|
||||||
|
|
||||||
this.physicsObjects.push({ mesh: modelClone, body })
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('Created', numInstances, 'instances of main_model with custom collision shapes')
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to load main_model.glb:', error)
|
|
||||||
console.log('Falling back to placeholder objects...')
|
|
||||||
this.createFallbackObjects()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private createFallbackObjects(): void {
|
|
||||||
// Fallback geometric objects if model loading fails
|
|
||||||
const geometries = [
|
|
||||||
new THREE.BoxGeometry(0.5, 0.5, 0.5),
|
|
||||||
new THREE.SphereGeometry(0.3, 16, 16),
|
|
||||||
new THREE.CylinderGeometry(0.2, 0.2, 0.6, 12),
|
|
||||||
]
|
|
||||||
|
|
||||||
const materials = [
|
|
||||||
new THREE.MeshStandardMaterial({ color: 0x4a9eff, metalness: 0.7, roughness: 0.3 }),
|
|
||||||
new THREE.MeshStandardMaterial({ color: 0xff6b4a, metalness: 0.5, roughness: 0.4 }),
|
|
||||||
new THREE.MeshStandardMaterial({ color: 0x4aff6b, metalness: 0.8, roughness: 0.2 }),
|
|
||||||
]
|
|
||||||
|
|
||||||
for (let i = 0; i < 15; i++) {
|
|
||||||
const geometry = geometries[i % geometries.length]
|
|
||||||
const material = materials[i % materials.length]
|
|
||||||
const mesh = new THREE.Mesh(geometry, material)
|
|
||||||
mesh.castShadow = true
|
|
||||||
mesh.receiveShadow = true
|
|
||||||
|
|
||||||
// Start objects close to the attraction point for immediate clustering
|
|
||||||
const x = (Math.random() - 0.5) * 6 // Reduced from 20 to 6
|
|
||||||
const y = (Math.random() - 0.5) * 6 // Reduced from 20 to 6
|
|
||||||
const z = (Math.random() - 0.5) * 6 // Reduced from 20 to 6
|
|
||||||
mesh.position.set(x, y, z)
|
|
||||||
|
|
||||||
// Create physics body with rotation enabled
|
|
||||||
const shape = new CANNON.Sphere(0.3)
|
|
||||||
const body = new CANNON.Body({
|
|
||||||
mass: 1,
|
|
||||||
material: this.world.defaultMaterial,
|
|
||||||
type: CANNON.Body.DYNAMIC
|
|
||||||
})
|
|
||||||
body.addShape(shape)
|
|
||||||
body.position.set(x, y, z)
|
|
||||||
|
|
||||||
// Enable rotational dynamics
|
|
||||||
body.angularDamping = 0.1
|
|
||||||
body.linearDamping = 0.05
|
|
||||||
|
|
||||||
body.velocity.set(
|
|
||||||
(Math.random() - 0.5) * 1, // Reduced from 2 to 1
|
|
||||||
(Math.random() - 0.5) * 1, // Reduced from 2 to 1
|
|
||||||
(Math.random() - 0.5) * 1 // Reduced from 2 to 1
|
|
||||||
)
|
|
||||||
|
|
||||||
// Add initial angular velocity
|
|
||||||
body.angularVelocity.set(
|
|
||||||
(Math.random() - 0.5) * 2,
|
|
||||||
(Math.random() - 0.5) * 2,
|
|
||||||
(Math.random() - 0.5) * 2
|
|
||||||
)
|
|
||||||
|
|
||||||
this.scene.add(mesh)
|
|
||||||
this.world.addBody(body)
|
|
||||||
|
|
||||||
this.physicsObjects.push({ mesh, body })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private setupEventListeners(): void {
|
|
||||||
// Mouse move event
|
|
||||||
window.addEventListener('mousemove', (event) => {
|
|
||||||
this.mouse.x = (event.clientX / window.innerWidth) * 2 - 1
|
|
||||||
this.mouse.y = -(event.clientY / window.innerHeight) * 2 + 1
|
|
||||||
|
|
||||||
// Update raycaster
|
|
||||||
this.raycaster.setFromCamera(this.mouse, this.camera)
|
|
||||||
|
|
||||||
// Calculate mouse position in world space
|
|
||||||
const distance = 10 // Distance from camera
|
|
||||||
this.mouseWorldPosition.copy(this.raycaster.ray.direction)
|
|
||||||
this.mouseWorldPosition.multiplyScalar(distance)
|
|
||||||
this.mouseWorldPosition.add(this.camera.position)
|
|
||||||
})
|
|
||||||
|
|
||||||
// Window resize event
|
|
||||||
window.addEventListener('resize', () => {
|
|
||||||
this.camera.aspect = window.innerWidth / window.innerHeight
|
|
||||||
this.camera.updateProjectionMatrix()
|
|
||||||
this.renderer.setSize(window.innerWidth, window.innerHeight)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
private updatePhysics(deltaTime: number): void {
|
|
||||||
// Use a smaller, more stable timestep for better collision detection
|
|
||||||
const fixedTimeStep = 1/60 // 60 Hz - more stable than 120 Hz
|
|
||||||
const maxSubSteps = 5 // Increased substeps for better collision accuracy
|
|
||||||
|
|
||||||
this.world.step(fixedTimeStep, deltaTime, maxSubSteps)
|
|
||||||
|
|
||||||
// Apply forces to objects
|
|
||||||
this.physicsObjects.forEach((obj) => {
|
|
||||||
// Attraction to center point
|
|
||||||
const attractionForce = new CANNON.Vec3()
|
|
||||||
attractionForce.x = this.attractionPoint.x - obj.body.position.x
|
|
||||||
attractionForce.y = this.attractionPoint.y - obj.body.position.y
|
|
||||||
attractionForce.z = this.attractionPoint.z - obj.body.position.z
|
|
||||||
|
|
||||||
const distance = Math.sqrt(
|
|
||||||
attractionForce.x * attractionForce.x +
|
|
||||||
attractionForce.y * attractionForce.y +
|
|
||||||
attractionForce.z * attractionForce.z
|
|
||||||
)
|
|
||||||
|
|
||||||
if (distance > 0) {
|
|
||||||
const strength = 16.0 / (distance * distance + 1) // Reduced from 8.0 to prevent objects moving too fast
|
|
||||||
attractionForce.scale(strength, attractionForce)
|
|
||||||
obj.body.force.set(
|
|
||||||
obj.body.force.x + attractionForce.x,
|
|
||||||
obj.body.force.y + attractionForce.y,
|
|
||||||
obj.body.force.z + attractionForce.z
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mouse repulsion
|
|
||||||
const repulsionForce = new CANNON.Vec3()
|
|
||||||
repulsionForce.x = obj.body.position.x - this.mouseWorldPosition.x
|
|
||||||
repulsionForce.y = obj.body.position.y - this.mouseWorldPosition.y
|
|
||||||
repulsionForce.z = obj.body.position.z - this.mouseWorldPosition.z
|
|
||||||
|
|
||||||
const mouseDistance = Math.sqrt(
|
|
||||||
repulsionForce.x * repulsionForce.x +
|
|
||||||
repulsionForce.y * repulsionForce.y +
|
|
||||||
repulsionForce.z * repulsionForce.z
|
|
||||||
)
|
|
||||||
|
|
||||||
if (mouseDistance < 3 && mouseDistance > 0) {
|
|
||||||
const repulsionStrength = 5.0 / (mouseDistance * mouseDistance + 0.1)
|
|
||||||
repulsionForce.scale(repulsionStrength, repulsionForce)
|
|
||||||
obj.body.force.set(
|
|
||||||
obj.body.force.x + repulsionForce.x,
|
|
||||||
obj.body.force.y + repulsionForce.y,
|
|
||||||
obj.body.force.z + repulsionForce.z
|
|
||||||
)
|
|
||||||
|
|
||||||
// Add rotational torque from mouse interaction
|
|
||||||
const torque = new CANNON.Vec3(
|
|
||||||
(Math.random() - 0.5) * repulsionStrength * 0.1,
|
|
||||||
(Math.random() - 0.5) * repulsionStrength * 0.1,
|
|
||||||
(Math.random() - 0.5) * repulsionStrength * 0.1
|
|
||||||
)
|
|
||||||
obj.body.torque.set(
|
|
||||||
obj.body.torque.x + torque.x,
|
|
||||||
obj.body.torque.y + torque.y,
|
|
||||||
obj.body.torque.z + torque.z
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply damping
|
|
||||||
obj.body.velocity.scale(0.99, obj.body.velocity)
|
|
||||||
|
|
||||||
// Update mesh position to match physics body
|
|
||||||
obj.mesh.position.copy(obj.body.position as any)
|
|
||||||
obj.mesh.quaternion.copy(obj.body.quaternion as any)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private animate(currentTime: number = 0): void {
|
private animate(currentTime: number = 0): void {
|
||||||
@@ -666,7 +117,14 @@ class AAFHomepage {
|
|||||||
// Clamp delta time to prevent large jumps
|
// Clamp delta time to prevent large jumps
|
||||||
const clampedDeltaTime = Math.min(deltaTime, 1/30)
|
const clampedDeltaTime = Math.min(deltaTime, 1/30)
|
||||||
|
|
||||||
this.updatePhysics(clampedDeltaTime)
|
PhysicsManager.updatePhysics(
|
||||||
|
this.world,
|
||||||
|
this.physicsObjects,
|
||||||
|
this.attractionPoint,
|
||||||
|
this.mouseWorldPosition,
|
||||||
|
clampedDeltaTime
|
||||||
|
)
|
||||||
|
|
||||||
this.renderer.render(this.scene, this.camera)
|
this.renderer.render(this.scene, this.camera)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
417
app/src/model-loader.ts
Normal file
417
app/src/model-loader.ts
Normal file
@@ -0,0 +1,417 @@
|
|||||||
|
import * as THREE from 'three'
|
||||||
|
import * as CANNON from 'cannon-es'
|
||||||
|
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'
|
||||||
|
import type { ModelGeometry, PhysicsObject } from './types'
|
||||||
|
|
||||||
|
export class ModelLoader {
|
||||||
|
private loader: GLTFLoader
|
||||||
|
private modelGeometry: ModelGeometry | null = null
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.loader = new GLTFLoader()
|
||||||
|
}
|
||||||
|
|
||||||
|
private extractModelGeometry(gltf: any): ModelGeometry | null {
|
||||||
|
const geometries: THREE.BufferGeometry[] = []
|
||||||
|
const boundingBox = new THREE.Box3()
|
||||||
|
|
||||||
|
// Update world matrices first
|
||||||
|
gltf.scene.updateMatrixWorld(true)
|
||||||
|
|
||||||
|
// Traverse the model to find all mesh geometries
|
||||||
|
gltf.scene.traverse((child: any) => {
|
||||||
|
if (child instanceof THREE.Mesh && child.geometry) {
|
||||||
|
// Apply the mesh's transform to the geometry
|
||||||
|
const geometry = child.geometry.clone() as THREE.BufferGeometry
|
||||||
|
|
||||||
|
// Only apply matrix if it's not identity
|
||||||
|
if (!child.matrixWorld.equals(new THREE.Matrix4())) {
|
||||||
|
geometry.applyMatrix4(child.matrixWorld)
|
||||||
|
}
|
||||||
|
|
||||||
|
geometries.push(geometry)
|
||||||
|
|
||||||
|
// Update bounding box
|
||||||
|
const positionAttr = geometry.getAttribute('position') as THREE.BufferAttribute
|
||||||
|
if (positionAttr) {
|
||||||
|
const geometryBoundingBox = new THREE.Box3().setFromBufferAttribute(positionAttr)
|
||||||
|
boundingBox.union(geometryBoundingBox)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (geometries.length === 0) {
|
||||||
|
console.warn('No geometries found in model')
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Found ${geometries.length} geometries to merge`)
|
||||||
|
|
||||||
|
// Merge all geometries
|
||||||
|
const combinedGeometry = this.mergeBufferGeometries(geometries)
|
||||||
|
|
||||||
|
// Extract vertices and faces
|
||||||
|
const positionAttribute = combinedGeometry.getAttribute('position') as THREE.BufferAttribute
|
||||||
|
if (!positionAttribute) {
|
||||||
|
console.error('No position attribute found in combined geometry')
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const vertices: number[] = Array.from(positionAttribute.array as Float32Array)
|
||||||
|
|
||||||
|
let indices: number[] = []
|
||||||
|
if (combinedGeometry.index) {
|
||||||
|
indices = Array.from(combinedGeometry.index.array)
|
||||||
|
} else {
|
||||||
|
// Generate indices if none exist
|
||||||
|
for (let i = 0; i < vertices.length / 3; i++) {
|
||||||
|
indices.push(i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Extracted ${vertices.length / 3} vertices and ${indices.length / 3} triangles`)
|
||||||
|
|
||||||
|
combinedGeometry.dispose()
|
||||||
|
|
||||||
|
return {
|
||||||
|
vertices,
|
||||||
|
indices,
|
||||||
|
boundingBox
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private mergeBufferGeometries(geometries: THREE.BufferGeometry[]): THREE.BufferGeometry {
|
||||||
|
const merged = new THREE.BufferGeometry()
|
||||||
|
const attributes: { [key: string]: THREE.BufferAttribute[] } = {}
|
||||||
|
let indexOffset = 0
|
||||||
|
const mergedIndices: number[] = []
|
||||||
|
|
||||||
|
// Collect all attributes
|
||||||
|
geometries.forEach(geometry => {
|
||||||
|
const attributeNames = Object.keys(geometry.attributes)
|
||||||
|
attributeNames.forEach(name => {
|
||||||
|
if (!attributes[name]) attributes[name] = []
|
||||||
|
const attr = geometry.attributes[name]
|
||||||
|
if (attr instanceof THREE.BufferAttribute) {
|
||||||
|
attributes[name].push(attr)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Handle indices
|
||||||
|
if (geometry.index) {
|
||||||
|
const indices = Array.from(geometry.index.array)
|
||||||
|
indices.forEach(index => mergedIndices.push(index + indexOffset))
|
||||||
|
indexOffset += geometry.attributes.position.count
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Merge attributes
|
||||||
|
Object.keys(attributes).forEach(name => {
|
||||||
|
const attributeArrays = attributes[name]
|
||||||
|
const itemSize = attributeArrays[0].itemSize
|
||||||
|
const normalized = attributeArrays[0].normalized
|
||||||
|
|
||||||
|
let totalCount = 0
|
||||||
|
attributeArrays.forEach(attr => totalCount += attr.count)
|
||||||
|
|
||||||
|
const mergedArray = new Float32Array(totalCount * itemSize)
|
||||||
|
let offset = 0
|
||||||
|
|
||||||
|
attributeArrays.forEach(attr => {
|
||||||
|
mergedArray.set(attr.array as Float32Array, offset)
|
||||||
|
offset += attr.array.length
|
||||||
|
})
|
||||||
|
|
||||||
|
merged.setAttribute(name, new THREE.BufferAttribute(mergedArray, itemSize, normalized))
|
||||||
|
})
|
||||||
|
|
||||||
|
if (mergedIndices.length > 0) {
|
||||||
|
merged.setIndex(mergedIndices)
|
||||||
|
}
|
||||||
|
|
||||||
|
return merged
|
||||||
|
}
|
||||||
|
|
||||||
|
private downsampleGeometry(geometry: ModelGeometry, factor: number = 0.3): ModelGeometry {
|
||||||
|
// Simple downsampling by taking every Nth vertex
|
||||||
|
const step = Math.max(1, Math.floor(1 / factor))
|
||||||
|
const downsampledVertices: number[] = []
|
||||||
|
const downsampledIndices: number[] = []
|
||||||
|
const vertexMap = new Map<number, number>()
|
||||||
|
|
||||||
|
// Downsample vertices
|
||||||
|
for (let i = 0; i < geometry.vertices.length; i += step * 3) {
|
||||||
|
if (i + 2 < geometry.vertices.length) {
|
||||||
|
const originalIndex = i / 3
|
||||||
|
const newIndex = downsampledVertices.length / 3
|
||||||
|
vertexMap.set(originalIndex, newIndex)
|
||||||
|
|
||||||
|
downsampledVertices.push(
|
||||||
|
geometry.vertices[i],
|
||||||
|
geometry.vertices[i + 1],
|
||||||
|
geometry.vertices[i + 2]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Downsample indices, keeping only triangles where all vertices are kept
|
||||||
|
for (let i = 0; i < geometry.indices.length; i += 3) {
|
||||||
|
const v1 = geometry.indices[i]
|
||||||
|
const v2 = geometry.indices[i + 1]
|
||||||
|
const v3 = geometry.indices[i + 2]
|
||||||
|
|
||||||
|
if (vertexMap.has(v1) && vertexMap.has(v2) && vertexMap.has(v3)) {
|
||||||
|
downsampledIndices.push(
|
||||||
|
vertexMap.get(v1)!,
|
||||||
|
vertexMap.get(v2)!,
|
||||||
|
vertexMap.get(v3)!
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
vertices: downsampledVertices,
|
||||||
|
indices: downsampledIndices,
|
||||||
|
boundingBox: geometry.boundingBox
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private createCollisionShape(geometry: ModelGeometry, scale: number = 1): CANNON.Shape {
|
||||||
|
try {
|
||||||
|
// For performance, use a simplified approach
|
||||||
|
// If the geometry is too complex, fall back to a hull or sphere
|
||||||
|
const vertexCount = geometry.vertices.length / 3
|
||||||
|
|
||||||
|
if (vertexCount > 100) {
|
||||||
|
// For complex meshes, use bounding box approximation
|
||||||
|
const size = geometry.boundingBox.getSize(new THREE.Vector3())
|
||||||
|
const scaledSize = size.multiplyScalar(scale)
|
||||||
|
return new CANNON.Box(new CANNON.Vec3(
|
||||||
|
scaledSize.x * 0.5,
|
||||||
|
scaledSize.y * 0.5,
|
||||||
|
scaledSize.z * 0.5
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
// For simpler meshes, try convex hull
|
||||||
|
const scaledVertices = geometry.vertices.map(v => v * scale)
|
||||||
|
const vertices: CANNON.Vec3[] = []
|
||||||
|
|
||||||
|
for (let i = 0; i < scaledVertices.length; i += 3) {
|
||||||
|
vertices.push(new CANNON.Vec3(
|
||||||
|
scaledVertices[i],
|
||||||
|
scaledVertices[i + 1],
|
||||||
|
scaledVertices[i + 2]
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Limit faces to prevent performance issues
|
||||||
|
const faces: number[][] = []
|
||||||
|
const maxFaces = Math.min(geometry.indices.length / 3, 50)
|
||||||
|
|
||||||
|
for (let i = 0; i < maxFaces * 3; i += 3) {
|
||||||
|
const v1 = geometry.indices[i]
|
||||||
|
const v2 = geometry.indices[i + 1]
|
||||||
|
const v3 = geometry.indices[i + 2]
|
||||||
|
|
||||||
|
if (v1 < vertices.length && v2 < vertices.length && v3 < vertices.length) {
|
||||||
|
faces.push([v1, v2, v3])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (faces.length > 0) {
|
||||||
|
const shape = new CANNON.ConvexPolyhedron({ vertices, faces })
|
||||||
|
return shape
|
||||||
|
} else {
|
||||||
|
throw new Error('No valid faces')
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Failed to create complex collision shape, falling back to sphere:', error)
|
||||||
|
// Fallback to sphere using bounding box
|
||||||
|
const size = geometry.boundingBox.getSize(new THREE.Vector3())
|
||||||
|
const radius = Math.max(size.x, size.y, size.z) * 0.5 * scale
|
||||||
|
return new CANNON.Sphere(Math.max(0.3, radius))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadAndCreateObjects(
|
||||||
|
scene: THREE.Scene,
|
||||||
|
world: CANNON.World,
|
||||||
|
physicsObjects: PhysicsObject[]
|
||||||
|
): Promise<void> {
|
||||||
|
try {
|
||||||
|
console.log('Loading main_model.glb...')
|
||||||
|
const gltf = await this.loader.loadAsync('/models/main_model.glb')
|
||||||
|
console.log('Model loaded successfully:', gltf)
|
||||||
|
|
||||||
|
// Extract and downsample the geometry for collision detection
|
||||||
|
const originalGeometry = this.extractModelGeometry(gltf)
|
||||||
|
if (originalGeometry) {
|
||||||
|
console.log('Extracted geometry with', originalGeometry.vertices.length / 3, 'vertices')
|
||||||
|
this.modelGeometry = this.downsampleGeometry(originalGeometry, 0.3) // 30% of original detail
|
||||||
|
console.log('Downsampled to', this.modelGeometry.vertices.length / 3, 'vertices for collision')
|
||||||
|
} else {
|
||||||
|
console.warn('Failed to extract geometry, will use simple collision shapes')
|
||||||
|
this.modelGeometry = null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create multiple instances of the loaded model
|
||||||
|
const numInstances = 20
|
||||||
|
|
||||||
|
gltf.scene.traverse((child) => {
|
||||||
|
if (child instanceof THREE.Mesh) {
|
||||||
|
child.castShadow = true
|
||||||
|
child.receiveShadow = true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
for (let i = 0; i < numInstances; i++) {
|
||||||
|
// Clone the model
|
||||||
|
const modelClone = gltf.scene.clone()
|
||||||
|
|
||||||
|
// Start objects close to the attraction point for immediate clustering
|
||||||
|
const x = (Math.random() - 0.5) * 6 // Reduced from 25 to 6
|
||||||
|
const y = (Math.random() - 0.5) * 6 // Reduced from 25 to 6
|
||||||
|
const z = (Math.random() - 0.5) * 6 // Reduced from 25 to 6
|
||||||
|
modelClone.position.set(x, y, z)
|
||||||
|
|
||||||
|
// Random scale variation (40% of current size)
|
||||||
|
const scale = (0.5 + Math.random() * 0.5) // Scaled down to 40%
|
||||||
|
modelClone.scale.setScalar(scale)
|
||||||
|
|
||||||
|
// Create physics body with custom collision shape or fallback to sphere
|
||||||
|
let shape: CANNON.Shape
|
||||||
|
if (this.modelGeometry) {
|
||||||
|
shape = this.createCollisionShape(this.modelGeometry, scale)
|
||||||
|
} else {
|
||||||
|
// Fallback to sphere if geometry extraction failed
|
||||||
|
const radius = Math.max(0.3, 0.8 * scale)
|
||||||
|
shape = new CANNON.Sphere(radius)
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = new CANNON.Body({
|
||||||
|
mass: 1,
|
||||||
|
material: world.defaultMaterial,
|
||||||
|
type: CANNON.Body.DYNAMIC
|
||||||
|
})
|
||||||
|
body.addShape(shape)
|
||||||
|
body.position.set(x, y, z)
|
||||||
|
|
||||||
|
// Enable rotational dynamics
|
||||||
|
body.angularDamping = 0.1 // Small damping for realistic rotation
|
||||||
|
body.linearDamping = 0.05 // Small linear damping
|
||||||
|
|
||||||
|
// Prevent objects from sleeping during collisions
|
||||||
|
body.sleepSpeedLimit = 0.1
|
||||||
|
body.sleepTimeLimit = 1
|
||||||
|
|
||||||
|
// Add collision event listener with rotational effects
|
||||||
|
body.addEventListener('collide', (event: any) => {
|
||||||
|
console.log('Collision detected!')
|
||||||
|
|
||||||
|
// Add some spin on collision for more dynamic movement
|
||||||
|
const impactStrength = event.contact?.getImpactVelocityAlongNormal() || 1
|
||||||
|
const spinForce = Math.min(impactStrength * 0.5, 2) // Cap the spin force
|
||||||
|
|
||||||
|
body.angularVelocity.set(
|
||||||
|
body.angularVelocity.x + (Math.random() - 0.5) * spinForce,
|
||||||
|
body.angularVelocity.y + (Math.random() - 0.5) * spinForce,
|
||||||
|
body.angularVelocity.z + (Math.random() - 0.5) * spinForce
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Add gentle initial velocity and rotation
|
||||||
|
body.velocity.set(
|
||||||
|
(Math.random() - 0.5) * 1, // Reduced from 3 to 1
|
||||||
|
(Math.random() - 0.5) * 1, // Reduced from 3 to 1
|
||||||
|
(Math.random() - 0.5) * 1 // Reduced from 3 to 1
|
||||||
|
)
|
||||||
|
|
||||||
|
// Add initial angular velocity for natural rotation
|
||||||
|
body.angularVelocity.set(
|
||||||
|
(Math.random() - 0.5) * 2,
|
||||||
|
(Math.random() - 0.5) * 2,
|
||||||
|
(Math.random() - 0.5) * 2
|
||||||
|
)
|
||||||
|
|
||||||
|
scene.add(modelClone)
|
||||||
|
world.addBody(body)
|
||||||
|
|
||||||
|
physicsObjects.push({ mesh: modelClone, body })
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Created', numInstances, 'instances of main_model with custom collision shapes')
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load main_model.glb:', error)
|
||||||
|
console.log('Falling back to placeholder objects...')
|
||||||
|
this.createFallbackObjects(scene, world, physicsObjects)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
createFallbackObjects(
|
||||||
|
scene: THREE.Scene,
|
||||||
|
world: CANNON.World,
|
||||||
|
physicsObjects: PhysicsObject[]
|
||||||
|
): void {
|
||||||
|
// Fallback geometric objects if model loading fails
|
||||||
|
const geometries = [
|
||||||
|
new THREE.BoxGeometry(0.5, 0.5, 0.5),
|
||||||
|
new THREE.SphereGeometry(0.3, 16, 16),
|
||||||
|
new THREE.CylinderGeometry(0.2, 0.2, 0.6, 12),
|
||||||
|
]
|
||||||
|
|
||||||
|
const materials = [
|
||||||
|
new THREE.MeshStandardMaterial({ color: 0x4a9eff, metalness: 0.7, roughness: 0.3 }),
|
||||||
|
new THREE.MeshStandardMaterial({ color: 0xff6b4a, metalness: 0.5, roughness: 0.4 }),
|
||||||
|
new THREE.MeshStandardMaterial({ color: 0x4aff6b, metalness: 0.8, roughness: 0.2 }),
|
||||||
|
]
|
||||||
|
|
||||||
|
for (let i = 0; i < 15; i++) {
|
||||||
|
const geometry = geometries[i % geometries.length]
|
||||||
|
const material = materials[i % materials.length]
|
||||||
|
const mesh = new THREE.Mesh(geometry, material)
|
||||||
|
mesh.castShadow = true
|
||||||
|
mesh.receiveShadow = true
|
||||||
|
|
||||||
|
// Start objects close to the attraction point for immediate clustering
|
||||||
|
const x = (Math.random() - 0.5) * 6 // Reduced from 20 to 6
|
||||||
|
const y = (Math.random() - 0.5) * 6 // Reduced from 20 to 6
|
||||||
|
const z = (Math.random() - 0.5) * 6 // Reduced from 20 to 6
|
||||||
|
mesh.position.set(x, y, z)
|
||||||
|
|
||||||
|
// Create physics body with rotation enabled
|
||||||
|
const shape = new CANNON.Sphere(0.3)
|
||||||
|
const body = new CANNON.Body({
|
||||||
|
mass: 1,
|
||||||
|
material: world.defaultMaterial,
|
||||||
|
type: CANNON.Body.DYNAMIC
|
||||||
|
})
|
||||||
|
body.addShape(shape)
|
||||||
|
body.position.set(x, y, z)
|
||||||
|
|
||||||
|
// Enable rotational dynamics
|
||||||
|
body.angularDamping = 0.1
|
||||||
|
body.linearDamping = 0.05
|
||||||
|
|
||||||
|
body.velocity.set(
|
||||||
|
(Math.random() - 0.5) * 1, // Reduced from 2 to 1
|
||||||
|
(Math.random() - 0.5) * 1, // Reduced from 2 to 1
|
||||||
|
(Math.random() - 0.5) * 1 // Reduced from 2 to 1
|
||||||
|
)
|
||||||
|
|
||||||
|
// Add initial angular velocity
|
||||||
|
body.angularVelocity.set(
|
||||||
|
(Math.random() - 0.5) * 2,
|
||||||
|
(Math.random() - 0.5) * 2,
|
||||||
|
(Math.random() - 0.5) * 2
|
||||||
|
)
|
||||||
|
|
||||||
|
scene.add(mesh)
|
||||||
|
world.addBody(body)
|
||||||
|
|
||||||
|
physicsObjects.push({ mesh, body })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
115
app/src/physics.ts
Normal file
115
app/src/physics.ts
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
import * as THREE from 'three'
|
||||||
|
import * as CANNON from 'cannon-es'
|
||||||
|
|
||||||
|
export class PhysicsManager {
|
||||||
|
static setupPhysicsWorld(): CANNON.World {
|
||||||
|
const world = new CANNON.World()
|
||||||
|
|
||||||
|
// Setup physics world
|
||||||
|
world.gravity.set(0, 0, 0)
|
||||||
|
|
||||||
|
// Use SAPBroadphase for better collision detection with many objects
|
||||||
|
world.broadphase = new CANNON.SAPBroadphase(world)
|
||||||
|
|
||||||
|
// Enable collision detection and response
|
||||||
|
world.allowSleep = false // Prevent objects from sleeping
|
||||||
|
|
||||||
|
// Configure contact material for better collisions
|
||||||
|
const defaultMaterial = new CANNON.Material('default')
|
||||||
|
const defaultContactMaterial = new CANNON.ContactMaterial(defaultMaterial, defaultMaterial, {
|
||||||
|
friction: 0.4,
|
||||||
|
restitution: 0.3,
|
||||||
|
contactEquationStiffness: 1e8,
|
||||||
|
contactEquationRelaxation: 3,
|
||||||
|
frictionEquationStiffness: 1e8,
|
||||||
|
frictionEquationRelaxation: 3
|
||||||
|
})
|
||||||
|
world.addContactMaterial(defaultContactMaterial)
|
||||||
|
world.defaultMaterial = defaultMaterial
|
||||||
|
|
||||||
|
return world
|
||||||
|
}
|
||||||
|
|
||||||
|
static updatePhysics(
|
||||||
|
world: CANNON.World,
|
||||||
|
physicsObjects: Array<{ mesh: THREE.Object3D; body: CANNON.Body }>,
|
||||||
|
attractionPoint: THREE.Vector3,
|
||||||
|
mouseWorldPosition: THREE.Vector3,
|
||||||
|
deltaTime: number
|
||||||
|
): void {
|
||||||
|
// Use a smaller, more stable timestep for better collision detection
|
||||||
|
const fixedTimeStep = 1/60 // 60 Hz - more stable than 120 Hz
|
||||||
|
const maxSubSteps = 5 // Increased substeps for better collision accuracy
|
||||||
|
|
||||||
|
world.step(fixedTimeStep, deltaTime, maxSubSteps)
|
||||||
|
|
||||||
|
// Apply forces to objects
|
||||||
|
physicsObjects.forEach((obj) => {
|
||||||
|
// Attraction to center point
|
||||||
|
const attractionForce = new CANNON.Vec3()
|
||||||
|
attractionForce.x = attractionPoint.x - obj.body.position.x
|
||||||
|
attractionForce.y = attractionPoint.y - obj.body.position.y
|
||||||
|
attractionForce.z = attractionPoint.z - obj.body.position.z
|
||||||
|
|
||||||
|
const distance = Math.sqrt(
|
||||||
|
attractionForce.x * attractionForce.x +
|
||||||
|
attractionForce.y * attractionForce.y +
|
||||||
|
attractionForce.z * attractionForce.z
|
||||||
|
)
|
||||||
|
|
||||||
|
if (distance > 0) {
|
||||||
|
const strength = 16.0 / (distance * distance + 1) // Reduced from 8.0 to prevent objects moving too fast
|
||||||
|
attractionForce.scale(strength, attractionForce)
|
||||||
|
obj.body.force.set(
|
||||||
|
obj.body.force.x + attractionForce.x,
|
||||||
|
obj.body.force.y + attractionForce.y,
|
||||||
|
obj.body.force.z + attractionForce.z
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mouse repulsion
|
||||||
|
const repulsionForce = new CANNON.Vec3()
|
||||||
|
repulsionForce.x = obj.body.position.x - mouseWorldPosition.x
|
||||||
|
repulsionForce.y = obj.body.position.y - mouseWorldPosition.y
|
||||||
|
repulsionForce.z = obj.body.position.z - mouseWorldPosition.z
|
||||||
|
|
||||||
|
const mouseDistance = Math.sqrt(
|
||||||
|
repulsionForce.x * repulsionForce.x +
|
||||||
|
repulsionForce.y * repulsionForce.y +
|
||||||
|
repulsionForce.z * repulsionForce.z
|
||||||
|
)
|
||||||
|
|
||||||
|
if (mouseDistance < 3 && mouseDistance > 0) {
|
||||||
|
const repulsionStrength = 5.0 / (mouseDistance * mouseDistance + 0.1)
|
||||||
|
repulsionForce.scale(repulsionStrength, repulsionForce)
|
||||||
|
obj.body.force.set(
|
||||||
|
obj.body.force.x + repulsionForce.x,
|
||||||
|
obj.body.force.y + repulsionForce.y,
|
||||||
|
obj.body.force.z + repulsionForce.z
|
||||||
|
)
|
||||||
|
|
||||||
|
// Debug log to verify mouse repulsion is working
|
||||||
|
console.log('Mouse repulsion applied, distance:', mouseDistance.toFixed(2), 'strength:', repulsionStrength.toFixed(2))
|
||||||
|
|
||||||
|
// Add rotational torque from mouse interaction
|
||||||
|
const torque = new CANNON.Vec3(
|
||||||
|
(Math.random() - 0.5) * repulsionStrength * 0.1,
|
||||||
|
(Math.random() - 0.5) * repulsionStrength * 0.1,
|
||||||
|
(Math.random() - 0.5) * repulsionStrength * 0.1
|
||||||
|
)
|
||||||
|
obj.body.torque.set(
|
||||||
|
obj.body.torque.x + torque.x,
|
||||||
|
obj.body.torque.y + torque.y,
|
||||||
|
obj.body.torque.z + torque.z
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply damping
|
||||||
|
obj.body.velocity.scale(0.99, obj.body.velocity)
|
||||||
|
|
||||||
|
// Update mesh position to match physics body
|
||||||
|
obj.mesh.position.copy(obj.body.position as any)
|
||||||
|
obj.mesh.quaternion.copy(obj.body.quaternion as any)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
13
app/src/types.ts
Normal file
13
app/src/types.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import * as THREE from 'three'
|
||||||
|
import * as CANNON from 'cannon-es'
|
||||||
|
|
||||||
|
export interface PhysicsObject {
|
||||||
|
mesh: THREE.Object3D
|
||||||
|
body: CANNON.Body
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ModelGeometry {
|
||||||
|
vertices: number[]
|
||||||
|
indices: number[]
|
||||||
|
boundingBox: THREE.Box3
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user