├── .gitignore ├── LICENSE ├── README.md ├── dist ├── physx.js └── physx.min.js ├── examples ├── README.md ├── basic │ ├── images │ │ ├── basketball-gray.png │ │ ├── crate.jpg │ │ ├── hexagons.png │ │ └── pixels.png │ └── index.html ├── components │ ├── force-pushable.js │ ├── grab.js │ └── rain-of-entities.js ├── compound │ └── index.html ├── constraints │ └── index.html ├── materials │ └── index.html ├── pinboard │ ├── ammo-vs-physx.html │ ├── ammo.html │ ├── physx.html │ └── pinboard.js ├── sandbox │ └── index.html ├── spring │ └── index.html ├── stress │ └── index.html ├── sweeper │ └── index.html └── ttl │ └── index.html ├── index.js ├── package-lock.json ├── package.json ├── src ├── physics.js └── physx.release.js ├── styles.css ├── wasm └── physx.release.wasm ├── webpack.config.js └── webpack.prod.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | node_modules 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Zach Capalbo 4 | Copyright (c) 2022 Lee Stemkoski 5 | Copyright (c) 2022 Diarmid Mackenzie 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # physx 2 | A-Frame physics using PhysX 3 | 4 | For examples of usage, see: 5 | 6 | [Examples](examples/README.md) 7 | 8 | 9 | 10 | ## Installation 11 | 12 | There is just one main JS module, `physx.min.js`, which triggers download of a specified additional wasm module. 13 | 14 | The URL for the PhysX wasm module needs to be specified on the `physx` component schema. 15 | 16 | ### Installation via Script Tags 17 | 18 | You can either download the module from the `dist` directory of this repo and the wasm file from the `wasm` directory and include them like this: 19 | 20 | ```html 21 | 22 | 23 | ``` 24 | 25 | Or you can download via JSDelivr CDN (specifying the version number you want to use) 26 | 27 | ```html 28 | 29 | 30 | ``` 31 | 32 | ### Installation via npm 33 | 34 | Install the dependency: 35 | 36 | `npm install @c-frame/physx` 37 | 38 | In your project import it: 39 | 40 | ```js 41 | import "@c-frame/physx"; 42 | ``` 43 | 44 | Copy `node_modules/@c-frame/physx/wasm` maybe in your public folder and then reference it like this: 45 | 46 | ```html 47 | 48 | ``` 49 | 50 | 51 | ## Build 52 | 53 | Clone this repo, and run 54 | 55 | `npm install` 56 | 57 | To run for development purposes, run: 58 | 59 | `npm run dev` or `npm start` 60 | 61 | Examples can be viewed at /examples 62 | 63 | To build (non minified and minified builds), run: 64 | 65 | `npm run dist` 66 | 67 | You don't need to run it in development. 68 | 69 | 70 | ## System `physx` 71 | 72 | Implements the a physics system using an emscripten compiled PhysX engine. 73 | 74 | If `autoLoad` is `true`, or when you call `startPhysX()`, the `physx` system will automatically load and initialize the physics system with reasonable defaults and a ground plane. All you have to do is add [`physx-body`](#component-physx-body) to the bodies that you want to be part of the simulation. The system will take try to take care of things like collision meshes, position updates, etc automatically. The simplest physics scene looks something like: 75 | 76 | ```html 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | ``` 85 | 86 | If you want a little more control over how things behave, you can set the [`physx-material`](#component-physx-material) component on the objects in your simulation, or use [`physx-joint`s](#component-physx-joint), [`physx-joint-constraint`s](#component-physx-joint-constraint) and [`physx-joint-driver`s](#component-physx-joint-driver) to add some complexity to your scene. 87 | 88 | If you need more low-level control, the PhysX bindings are exposed through the `PhysX` property of the system. So for instance, if you wanted to make use of the [`PxCapsuleGeometry`](https://nvidiagameworks.github.io/PhysX/4.1/documentation/physxapi/files/classPxCapsuleGeometry.html) in your own component, you would call: 89 | 90 | ```js 91 | let myGeometry = new this.el.sceneEl.systems.physx.PhysX.PxCapsuleGeometry(1.0, 2.0) 92 | ``` 93 | 94 | The system uses [a fork](https://github.com/c-frame/PhysXSDK) of PhysX, built using the [Docker Wrapper](https://github.com/c-frame/physx-js). To see what's exposed to JavaScript, see [PxWebBindings.cpp](https://github.com/c-frame/PhysXSDK/blob/emscripten_wip/physx/source/physxwebbindings/src/PxWebBindings.cpp) 95 | 96 | It is also helpful to refer to the [NVIDIA PhysX documentation](https://nvidiagameworks.github.io/PhysX/4.1/documentation/physxguide/Index.html) 97 | 98 | ### physx Schema 99 | 100 | | Property | Type | Default | Description | 101 | | --------------------- | ---------------- | ------------------------------------------------------------ | ------------------------------------------------------------ | 102 | | delay | number | 5000 | Amount of time to wait after loading before starting the physics. Can be useful if there is still some things loading or initializing elsewhere in the scene | 103 | | throttle | number | 10 | Throttle for running the physics simulation. On complex scenes, you can increase this to avoid dropping video frames | 104 | | autoLoad | boolean | false | If true, the PhysX will automatically be loaded and started. If false, you will have to call `startPhysX()` manually to load and start the physics engine | 105 | | speed | number | 1 | Simulation speed multiplier. Increase or decrease to speed up or slow down simulation time | 106 | | wasmUrl | string | ../../wasm/physx.release.wasm (only useful for the examples) | URL for the PhysX WASM bundle. Be sure this matches the script version. | 107 | | useDefaultScene | boolean | true | If true, sets up a default scene with a ground plane and bounding cylinder. | 108 | | wrapBounds | boolean | false | NYI | 109 | | groundCollisionLayers | string | | Which collision layers the ground belongs to | 110 | | groundCollisionMask | string | | Which collision layers will collide with the ground | 111 | | gravity | vec3 | { x: 0, y: -9.8, z: 0 } | Global gravity vector | 112 | | stats | array of strings | | Where to output performance stats (if any), `panel`, `console`, `events` (or some combination).
- `panel` output stats to a panel similar to the A-Frame stats panel.
-`events` generates `physics-tick-timer` events, which can be processed externally.
-`console`outputs stats to the console. | 113 | 114 | ### physx Methods 115 | 116 | | Signature | Description | 117 | | --------------- | ------------------------------------- | 118 | | startPhysX() | Loads PhysX and starts the simulation | 119 | 120 | ------ 121 | 122 | 123 | 124 | ## Component `physx-material` 125 | 126 | Controls physics properties for individual shapes or rigid bodies. You can set this either on an entity with the `phyx-body` component, or on a shape or model contained in an entity with the `physx-body` component. If it's set on a `physx-body`, it will be the default material for all shapes in that body. If it's set on an element containing geometry or a model, it will be the material used for that shape only. 127 | 128 | For instance, in the following scene fragment: 129 | 130 | ```html 131 | 132 | 133 | 134 | 135 | 136 | ``` 137 | 138 | `shape1`, which is part of the `bodyA` rigid body, will have static friction of 1.0, since it has a material set on it. `shape2`, which is also part of the `bodyA` rigid body, will have a static friction of 0.5, since that is the body default. `bodyB` will have the component default of 0.2, since it is a separate body. 139 | 140 | ### physx-material Schema 141 | 142 | | Property | Type | Default | Description | 143 | | ------------------ | ------ | -------------- | ------------------------------------------------------------ | 144 | | staticFriction | number | 0.2 | Static friction | 145 | | dynamicFriction | number | 0.2 | Dynamic friction | 146 | | restitution | number | 0.2 | Restitution, or "bounciness" | 147 | | density | number | | Density for the shape. If densities are specified for *all* shapes in a rigid body, then the rigid body's mass properties will be automatically calculated based on the different densities. However, if density information is not specified for every shape, then the mass defined in the overarching [`physx-body`](#component-physx-body) will be used instead. | 148 | | collisionLayers | array | [1] | Which collision layers this shape is present on | 149 | | collidesWithLayers | array | [ 1, 2, 3, 4 ] | Array containing all layers that this shape should collide with | 150 | | collisionGroup | number | 0 | If `collisionGroup` is greater than 0, this shape will *not* collide with any other shape with the same `collisionGroup` value | 151 | | contactOffset | string | | If >= 0, this will set the PhysX contact offset, indicating how far away from the shape simulation contact events should begin. | 152 | | restOffset | string | | If >= 0, this will set the PhysX rest offset | 153 | 154 | ------ 155 | 156 | 157 | 158 | ## Component `physx-body` 159 | 160 | Turns an entity into a PhysX rigid body. This is the main component for creating physics objects. 161 | 162 | **Types** 163 | 164 | There are 3 types of supported rigid bodies. The type can be set by using the `type` proeprty, but once initialized cannot be changed. 165 | 166 | - `dynamic` objects are objects that will have physics simulated on them. The entity's world position, scale, and rotation will be used as the starting condition for the simulation, however once the simulation starts the entity's position and rotation will be replaced each frame with the results of the simulation. 167 | - `static` objects are objects that cannot move. They can be used to create collidable objects for `dynamic` objects, or for anchor points for joints. 168 | - `kinematic` objects are objects that can be moved programmatically, but will not be moved by the simulation. They can however, interact with and collide with dynamic objects. Each frame, the entity's `object3D` will be used to set the position and rotation for the simulation object. 169 | 170 | **Shapes** 171 | 172 | When the component is initialized, and on the `object3dset` event, all visible meshes that are descendents of this entity will have shapes created for them. Each individual mesh will have its own convex hull automatically generated for it. This means you can have reasonably accurate collision meshes both from building up shapes with a-frame geometry primitives, and from importing 3D models. 173 | 174 | Visible meshes can be excluded from this shape generation process by setting the `physx-no-collision` attribute on the corresponding `a-entity` element. Invisible meshes can be included into this shape generation process by settingt the `physx-hidden-collision` attribute on the corresponding `a-entity` element. This can be especially useful when using an external tool (like [Blender V-HACD](https://github.com/andyp123/blender_vhacd)) to create a low-poly convex collision mesh for a high-poly or concave mesh. This leads to this pattern for such cases: 175 | 176 | ```html 177 | 178 | 179 | 180 | 181 | ``` 182 | 183 | Note, in such cases that if you are setting material properties on individual shapes, then the property should go on the collision mesh entity 184 | 185 | ### physx-body Schema 186 | 187 | | Property | Type | Default | Description | 188 | | ------------------- | ------- | -------------------- | ------------------------------------------------------------ | 189 | | type | string | dynamic | **[dynamic, static, kinematic]** Type of the rigid body to create | 190 | | mass | number | 1 | Total mass of the body | 191 | | angularDamping | number | 0 | If > 0, will set the rigid body's angular damping | 192 | | linearDamping | number | 0 | If > 0, will set the rigid body's linear damping | 193 | | emitCollisionEvents | boolean | false | If set to `true`, it will emit `contactbegin` and `contactend` events when collisions occur | 194 | | highPrecision | boolean | false | If set to `true`, the object will receive extra attention by the simulation engine (at a performance cost). | 195 | | shapeOffset | vec3 | { x: 0, y: 0, z: 0 } | | 196 | 197 | ### physx-body Methods 198 | 199 | | Signature | Description | 200 | | ------------------ | ------------------------ | 201 | | toggleGravity `()` | Turns gravity on and off | 202 | 203 | ------ 204 | 205 | 206 | 207 | ## Component `physx-joint-driver` 208 | 209 | Creates a driver which exerts force to return the joint to the specified (currently only the initial) position with the given velocity characteristics. 210 | 211 | This can only be used on an entity with a `physx-joint` component. Currently only supports **D6** joint type. E.g. 212 | 213 | ```html 214 | 215 | 218 | 219 | 220 | ``` 221 | 222 | ### physx-joint-driver Schema 223 | 224 | | Property | Type | Default | Description | 225 | | --------------- | ------- | ---------------------- | ------------------------------------------------------------ | 226 | | axes | array | [] | Which axes the joint should operate on. Should be some combination of `x`, `y`, `z`, `twist`, `swing` | 227 | | stiffness | number | 1 | How stiff the drive should be | 228 | | damping | number | 1 | Damping to apply to the drive | 229 | | forceLimit | number | 3.4028234663852886e+38 | Maximum amount of force used to get to the target position | 230 | | useAcceleration | boolean | true | If true, will operate directly on body acceleration rather than on force | 231 | | linearVelocity | vec3 | { x: 0, y: 0, z: 0 } | Target linear velocity relative to the joint | 232 | | angularVelocity | vec3 | { x: 0, y: 0, z: 0 } | Targget angular velocity relative to the joint | 233 | | lockOtherAxes | boolean | false | If true, will automatically lock axes which are not being driven | 234 | | slerpRotation | boolean | true | If true SLERP rotation mode. If false, will use SWING mode. | 235 | 236 | ------ 237 | 238 | 239 | 240 | ## Component `physx-joint-constraint` 241 | 242 | Adds a constraint to a [`physx-joint`](#component-physx-joint). 243 | Supported joints are **D6**, **Revolute** and **Prismatic**. 244 | Can only be used on an entity with the `physx-joint` component. 245 | 246 | ### D6 joint constraint 247 | 248 | You can set multiple constraints per joint. Note that in order to specify attributes of individual axes, you will need to use multiple constraints. For instance: 249 | 250 | ```html 251 | 258 | 264 | 265 | ``` 266 | 267 | In the above example, the box will be able to move from -1 to 0.2 in both the x and z direction. It will be able to move from -1 to 0 in the y direction (relative to parent position), but this will be a soft constraint, subject to spring forces if the box goes past in the y direction. All rotation will be locked. (Note that since no target is specified, it will use the scene default target, effectively jointed to joint's initial position in the world) 268 | 269 | ### Revolute joint constraint 270 | 271 | Example of a door with an angular limit between -110 and 80 degrees: 272 | 273 | ```html 274 | 278 | 285 | 289 | 290 | 291 | ``` 292 | 293 | ### Prismatic joint constraint 294 | 295 | Slider example with position between -0.2 and 0.8 from the initial position: 296 | 297 | ```html 298 | 304 | 305 | 310 | 313 | 314 | 315 | ``` 316 | 317 | ### physx-joint-constraint Schema 318 | 319 | | Property | Type | Default | Description | 320 | | --------------- | ------ | ------- | ------------------------------------------------------------ | 321 | | lockedAxes | array | [] | [D6] Which axes are explicitly locked by this constraint and can't be moved at all. Should be some combination of `x`, `y`, `z`, `twist`, `swing` | 322 | | constrainedAxes | array | [] | [D6] Which axes are constrained by this constraint. These axes can be moved within the set limits. Should be some combination of `x`, `y`, `z`, `twist`, `swing` | 323 | | freeAxes | array | [] | [D6] Which axes are explicitly freed by this constraint. These axes will not obey any limits set here. Should be some combination of `x`, `y`, `z`, `twist`, `swing` | 324 | | linearLimit | vec2 | | [D6, Prismatic] Limit on linear movement. Only affects `x`, `y`, and `z` axes. First vector component is the minimum allowed position | 325 | | angularLimit | vec2 | | [Revolute] Limit on angular movement in degrees. First vector component is the minimum allowed angle, second is the maximum | 326 | | limitCone | vec2 | | [D6] Two angles in degrees specifying a cone in which the joint is allowed to swing, like a pendulum. | 327 | | twistLimit | vec2 | | [D6] Minimum and maximum angles in degrees that the joint is allowed to twist | 328 | | damping | number | 0 | [All] Spring damping for soft constraints | 329 | | restitution | number | 0 | [All] Spring restitution for soft constraints | 330 | | stiffness | number | 0 | [All] If greater than 0, will make this joint a soft constraint, and use a spring force model | 331 | 332 | ------ 333 | 334 | 335 | 336 | ## Component `physx-joint` 337 | 338 | Creates a PhysX joint between an ancestor rigid body and a target rigid body. 339 | 340 | The `physx-joint` is designed to be used either on or within an entity with the `physx-body` component. For instance: 341 | 342 | ``` 343 | 344 | 345 | 346 | ``` 347 | 348 | The position and rotation of the `physx-joint` will be used to create the corresponding PhysX joint object. Multiple joints can be created on a body, and multiple joints can target a body. 349 | 350 | **Stapler Example** 351 | 352 | Here's a simplified version of the stapler from the [physics playground demo]() 353 | 354 | ```html 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | ``` 363 | 364 | Notice the joint is created between the top part of the stapler (which contains the joint) and the bottom part of the stapler at the position of the `physx-joint` component's entity. This will be the pivot point for the stapler's rotation. 365 | 366 | ![Stapler with joint highlighted](https://vartiste.xyz/bce1dc6210d4b9aa9db6.png) 367 | 368 | ### physx-joint Schema 369 | 370 | | Property | Type | Default | Description | 371 | | ----------------- | -------- | ---------------- | ------------------------------------------------------------ | 372 | | type | string | Spherical | Rigid body joint type to use. See the [NVIDIA PhysX joint documentation](https://gameworksdocs.nvidia.com/PhysX/4.0/documentation/PhysXGuide/Manual/Joints.html) for details on each type | 373 | | target | selector | | Target object. If specified, must be an entity having the `physx-body` component. If no target is specified, a scene default target will be used, essentially joining the joint to its initial position in the world. | 374 | | breakForce | vec2 | { x: -1, y: -1 } | Force needed to break the constraint. First component is the linear force, second component is angular force in degrees. Set both components are >= 0 | 375 | | removeElOnBreak | boolean | false | If true, removes the entity containing this component when the joint is broken. | 376 | | collideWithTarget | boolean | false | If false, collision will be disabled between the rigid body containing the joint and the target rigid body. | 377 | | softFixed | boolean | false | When used with a D6 type, sets up a "soft" fixed joint. E.g., for grabbing things | 378 | | projectionTolerance | vec2 | { x: -1, y: -1 } | Kinematic projection, which forces joint back into alignment when the solver fails. First component is the linear tolerance in meters, second component is angular tolerance in degrees. Set both components are >= 0 | 379 | 380 | 381 | 382 | ## Collision Events 383 | 384 | If `emitCollisionEvents` is set of a `physx-body` then `contactbegin` and `contactend` events are emitted on that entity when it collides with other bodies. 385 | 386 | --------- 387 | 388 | Note the important restriction that **SDK state changes are not permitted from an event callback** ([full details here](https://gameworksdocs.nvidia.com/PhysX/4.1/documentation/physxguide/Manual/Simulation.html#callback-sequence)). This means that you should never add / remove / modify bodies, materials, joints, etc. directly from a collision event callback. 389 | 390 | If you need to make such changes in response to a collision event, you can use setTimeout() with a zero timer to delay the update until the physics processing for this tick has completed. 391 | 392 | -------- 393 | 394 | These events include a `detail` object with the following properties: 395 | 396 | | Property | Description | 397 | | -------------- | ------------------------------------------------------------ | 398 | | thisShape | A `PxShape` for the shape within this body involved in the collision. This can be used to look up the `physx-body` that owns the shape using `this.shapeMap` on the `physx` component on the scene. For a compound shape, there is currently no way to map this back to a specific object3D that corresponds to the shape in question. | 399 | | otherShape | A `PxShape` for the shape within the other body involved in the collision. See row above for what can be done with this. | 400 | | points | The set of contact points, a `PxVec3Vector`. Read length from `points.size()`, then access using `points.get(index)` for each index < `size`, which returns a JS object with properties x, y & z (like a THREE.Vector3, but not actually one).

This will be null on a `contactend` event. | 401 | | impulses | The set of impulses at these contact points, a `VectorPxReal`. Read length from `impulses.size()`, then access using `impulses.get(index)` for each index < `size`, which returns a number.

This will be null on a `contactend` event. | 402 | | otherComponent | The `physx-body` component for the other object in the collision. | 403 | 404 | 405 | 406 | ## Statistics 407 | 408 | The following statistics are available from PhysX. Each of these is refreshed every 100 ticks (i.e. every 100 frames). 409 | 410 | | Statistic | Meaning | 411 | | --------- | ------------------------------------------------------------ | 412 | | Static | The number of static bodies being handled by the physics engine. | 413 | | Dynamic | The number of dynamic bodies being handled by the physics engine. | 414 | | Kinematic | The number of kinematic bodies being handled by the physics engine. | 415 | | After | The number of milliseconds per tick after invoking the physics engine. Typically this is the time taken to synchronize the physics engine state into the scene, e.g. movements of dynamic bodies.
Median = median value in the last 100 ticks
90th % = 90th percentile value in the last 100 ticks
99th % = maximum recorded value over the last 100 ticks.
Note that unlike the physics implementations in aframe-physics-system, PhysX has no significant per-frame processing before invoking the physics engine, so the "after" statistic accounts for all the work done outside the PhysX WASM code. | 416 | | Engine | The number of milliseconds per tick actually running the physics engine.
Reported as Median / 90th / 99th percentiles, as above. | 417 | | Total | The total number of milliseconds of physics processing per tick: Engine + After. Reported as Median / 90th / 99th percentiles, as above. | 418 | 419 | 420 | 421 | 422 | 423 | ## Acknowledgements 424 | 425 | This repository is based on an original implementation of A-Frame physics using PhysX by [Zach Capalbo](https://vartiste.xyz/docs.html#physics.js) 426 | 427 | Simplification into a standalone codebase by [Lee Stemkoski](https://stemkoski.github.io/A-Frame-Examples/) 428 | 429 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | 3 | The following examples demonstrate use of `physx` 4 | 5 | This set of examples is based on [these examples from aframe-physics-system](https://c-frame.github.io/aframe-physics-system/examples/). 6 | 7 | | Example | Status | 8 | | ------------------------------------------------------------ | ------------------------------------------------------------ | 9 | | Basic example of some objects in a scene using PhysX | [**OK**](https://c-frame.github.io/physx/examples/basic/index.html) | 10 | | Sandbox example - demonstration of many features in a single example. | [**Some issues**](https://c-frame.github.io/physx/examples/sandbox/index.html) | 11 | | Construct a [compound shape](https://c-frame.github.io/aframe-physics-system/#shape) and simulate collision with a ground plane. | [**OK**](https://c-frame.github.io/physx/examples/compound/index.html) | 12 | | Demonstration of many PhysX constraints including Fixed, Revolute, Spherical and Prismatic constraints. | [**OK**](https://c-frame.github.io/physx/examples/constraints/index.html) | 13 | | Bounce simulation with restitution (bounciness) of 1. | [**OK**](https://c-frame.github.io/physx/examples/materials/index.html) | 14 | | Four vertical [springs](https://c-frame.github.io/aframe-physics-system/#spring) each between two boxes with an assortment of damping and stiffness values | [**OK**](https://c-frame.github.io/physx/examples/spring/index.html) | 15 | | Apply [strong impulse](https://c-frame.github.io/aframe-physics-system/#using-the-cannonjs-api) to a cube when the user clicks with a mouse. Cubes are arranged in four 4x3 walls. | [**OK**](https://c-frame.github.io/physx/examples/stress/index.html) | 16 | | Animate a long wall moving along the z-axis along the initial view direction. | [**OK**](https://c-frame.github.io/physx/examples/sweeper/index.html) | 17 | | Remove a [dynamic body](https://c-frame.github.io/aframe-physics-system/#dynamic-body-and-static-body) from the scene after 100 frames | [**OK**](https://c-frame.github.io/physx/examples/ttl/index.html) | 18 | | Performance test: 100 balls rolling down a peg board, with timing data from physics engine. | [**OK**](https://c-frame.github.io/physx/examples/pinboard/physx.html) | 19 | | Performance comparison: performance of PhysX vs aframe-physics-system with the Ammo driver. | [**OK**](https://c-frame.github.io/physx/examples/pinboard/ammo-vs-physx.html) | 20 | 21 | 22 | 23 | ## Acknowledgements 24 | 25 | Basic example (basketballs) created [Lee Stemkoski](https://stemkoski.github.io/A-Frame-Examples/) 26 | 27 | Several examples are derived from aframe-physics-system examples, which were originally created by [DonMcCurdy](https://github.com/donmccurdy). 28 | 29 | 30 | 31 | This site is open source. [Improve this page](https://github.com/c-frame/aframe-physics-system/edit/master/examples/README.md). 32 | -------------------------------------------------------------------------------- /examples/basic/images/basketball-gray.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c-frame/physx/383ff2c335ff48216d02092c60d35fc3386019ff/examples/basic/images/basketball-gray.png -------------------------------------------------------------------------------- /examples/basic/images/crate.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c-frame/physx/383ff2c335ff48216d02092c60d35fc3386019ff/examples/basic/images/crate.jpg -------------------------------------------------------------------------------- /examples/basic/images/hexagons.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c-frame/physx/383ff2c335ff48216d02092c60d35fc3386019ff/examples/basic/images/hexagons.png -------------------------------------------------------------------------------- /examples/basic/images/pixels.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c-frame/physx/383ff2c335ff48216d02092c60d35fc3386019ff/examples/basic/images/pixels.png -------------------------------------------------------------------------------- /examples/basic/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | A-Frame: Physics 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 |

Basic example showing use of PhysX in a scene

16 |

Example created by Lee Stemkoski

17 |
18 | 21 | view code 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 41 | 42 | 43 | 44 | 48 | 49 | 50 | 54 | 55 | 56 | 60 | 61 | 62 | 66 | 67 | 68 | 69 | 70 | 73 | 74 | 75 | 79 | 80 | 81 | 85 | 86 | 87 | 91 | 92 | 93 | 97 | 98 | 99 | 103 | 104 | 105 | 109 | 110 | 111 | 115 | 116 | 117 | 118 | 119 | 120 | -------------------------------------------------------------------------------- /examples/components/force-pushable.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Force Pushable component. 3 | * 4 | * Based on an original component by Don McCurdy in aframe-physics-system 5 | * 6 | * Copyright (c) 2016 Don McCurdy 7 | * 8 | * Applies behavior to the current entity such that cursor clicks will apply a 9 | * strong impulse, pushing the entity away from the viewer. 10 | * 11 | * Requires: physx 12 | */ 13 | AFRAME.registerComponent('physx-force-pushable', { 14 | schema: { 15 | force: { default: 10 } 16 | }, 17 | init: function () { 18 | 19 | this.pStart = new THREE.Vector3(); 20 | this.sourceEl = this.el.sceneEl.querySelector('[camera]'); 21 | this.forcePushPhysX = this.forcePushPhysX.bind(this); 22 | 23 | 24 | this.sourcePosition = new THREE.Vector3(); 25 | this.force = new THREE.Vector3(); 26 | this.pos = new THREE.Vector3(); 27 | }, 28 | 29 | play() { 30 | this.el.addEventListener('click', this.forcePushPhysX); 31 | }, 32 | 33 | pause() { 34 | this.el.removeEventListener('click', this.forcePushPhysX); 35 | }, 36 | 37 | forcePushPhysX: function (e) { 38 | 39 | const el = this.el 40 | if (!el.components['physx-body']) return 41 | const body = el.components['physx-body'].rigidBody 42 | if (!body) return 43 | 44 | const force = this.force 45 | const source = this.sourcePosition 46 | 47 | // WebXR requires care getting camera position https://github.com/mrdoob/three.js/issues/18448 48 | source.setFromMatrixPosition( this.sourceEl.object3D.matrixWorld ); 49 | 50 | el.object3D.getWorldPosition(force) 51 | force.sub(source) 52 | 53 | force.normalize(); 54 | 55 | // not sure about units, but force seems stronger with PhysX than Cannon, so scaling down 56 | // by a factor of 5. 57 | force.multiplyScalar(this.data.force / 5); 58 | 59 | // use data from intersection to determine point at which to apply impulse. 60 | const pos = this.pos 61 | pos.copy(e.detail.intersection.point) 62 | el.object3D.worldToLocal(pos) 63 | 64 | body.addImpulseAtLocalPos(force, pos); 65 | } 66 | }); 67 | -------------------------------------------------------------------------------- /examples/components/grab.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Grab component. 3 | * 4 | * Based on an original component by Don McCurdy in aframe-physics-system 5 | * 6 | * Copyright (c) 2016 Don McCurdy 7 | */ 8 | 9 | AFRAME.registerComponent('physx-grab', { 10 | init: function () { 11 | 12 | // If a state of "grabbed" is set on a physx-body entity, 13 | // the entity is automatically transformed into a kinematic entity. 14 | // To avoid triggering this (we want to grab using constraints, and leave the 15 | // body as dynamic), we use a non-clashing name for the state we set on the entity when 16 | // grabbing it. 17 | this.GRABBED_STATE = 'grabbed-dynamic'; 18 | 19 | this.grabbing = false; 20 | this.hitEl = /** @type {AFRAME.Element} */ null; 21 | 22 | // Bind event handlers 23 | this.onHit = this.onHit.bind(this); 24 | this.onGripOpen = this.onGripOpen.bind(this); 25 | this.onGripClose = this.onGripClose.bind(this); 26 | 27 | }, 28 | 29 | play: function () { 30 | var el = this.el; 31 | el.addEventListener('contactbegin', this.onHit); 32 | el.addEventListener('gripdown', this.onGripClose); 33 | el.addEventListener('gripup', this.onGripOpen); 34 | el.addEventListener('trackpaddown', this.onGripClose); 35 | el.addEventListener('trackpadup', this.onGripOpen); 36 | el.addEventListener('triggerdown', this.onGripClose); 37 | el.addEventListener('triggerup', this.onGripOpen); 38 | }, 39 | 40 | pause: function () { 41 | var el = this.el; 42 | el.removeEventListener('contactbegin', this.onHit); 43 | el.removeEventListener('gripdown', this.onGripClose); 44 | el.removeEventListener('gripup', this.onGripOpen); 45 | el.removeEventListener('trackpaddown', this.onGripClose); 46 | el.removeEventListener('trackpadup', this.onGripOpen); 47 | el.removeEventListener('triggerdown', this.onGripClose); 48 | el.removeEventListener('triggerup', this.onGripOpen); 49 | }, 50 | 51 | onGripClose: function (evt) { 52 | this.grabbing = true; 53 | }, 54 | 55 | onGripOpen: function (evt) { 56 | var hitEl = this.hitEl; 57 | this.grabbing = false; 58 | if (!hitEl) { return; } 59 | hitEl.removeState(this.GRABBED_STATE); 60 | 61 | this.hitEl = undefined; 62 | 63 | this.removeJoint() 64 | }, 65 | 66 | onHit: function (evt) { 67 | var hitEl = evt.detail.otherComponent.el; 68 | // If the element is already grabbed (it could be grabbed by another controller). 69 | // If the hand is not grabbing the element does not stick. 70 | // If we're already grabbing something you can't grab again. 71 | if (!hitEl || hitEl.is(this.GRABBED_STATE) || !this.grabbing || this.hitEl) { return; } 72 | hitEl.addState(this.GRABBED_STATE); 73 | this.hitEl = hitEl; 74 | 75 | this.addJoint(hitEl, evt.target) 76 | }, 77 | 78 | addJoint(el, target) { 79 | 80 | this.removeJoint() 81 | 82 | this.joint = document.createElement('a-entity') 83 | this.joint.setAttribute("physx-joint", `type: Fixed; target: #${target.id}`) 84 | 85 | el.appendChild(this.joint) 86 | }, 87 | 88 | removeJoint() { 89 | 90 | if (!this.joint) return 91 | this.joint.parentElement.removeChild(this.joint) 92 | this.joint = null 93 | } 94 | }); 95 | -------------------------------------------------------------------------------- /examples/components/rain-of-entities.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Based on an original component by Don McCurdy in aframe-physics-system 3 | * 4 | * Copyright (c) 2016 Don McCurdy 5 | * 6 | * Rain of Entities component. 7 | * 8 | * Creates a spawner on the scene, which periodically generates new entities 9 | * and drops them from the sky. Objects falling below altitude=0 will be 10 | * recycled after a few seconds. 11 | * 12 | * Requires: physics 13 | */ 14 | AFRAME.registerComponent('rain-of-entities', { 15 | schema: { 16 | tagName: { default: 'a-box' }, 17 | components: { default: ['dynamic-body', 'force-pushable', 'color|#39BB82', 'scale|0.2 0.2 0.2'] }, 18 | spread: { default: 10, min: 0 }, 19 | maxCount: { default: 10, min: 0 }, 20 | interval: { default: 1000, min: 0 }, 21 | lifetime: { default: 10000, min: 0 } 22 | }, 23 | init: function () { 24 | this.boxes = []; 25 | this.timeout = setInterval(this.spawn.bind(this), this.data.interval); 26 | }, 27 | spawn: function () { 28 | if (this.boxes.length >= this.data.maxCount) { 29 | clearInterval(this.timeout); 30 | this.timeout= null; 31 | return; 32 | } 33 | 34 | var data = this.data, 35 | box = document.createElement(data.tagName); 36 | 37 | this.boxes.push(box); 38 | this.el.appendChild(box); 39 | 40 | box.setAttribute('position', this.randomPosition()); 41 | data.components.forEach(function (s) { 42 | var parts = s.split('|'); 43 | box.setAttribute(parts[0], parts[1] || ''); 44 | }); 45 | 46 | // Recycling is important, kids. 47 | setInterval(function () { 48 | if (box.object3D.position.y > 0) return; 49 | this.recycleBox(box); 50 | }.bind(this), this.data.lifetime); 51 | 52 | }, 53 | randomPosition: function () { 54 | var spread = this.data.spread; 55 | return { 56 | x: Math.random() * spread - spread / 2, 57 | y: 3, 58 | z: Math.random() * spread - spread / 2 59 | }; 60 | }, 61 | 62 | recycleBox(box) { 63 | 64 | 65 | box.removeAttribute("physx-body") 66 | box.object3D.position.copy(this.randomPosition()); 67 | box.object3D.quaternion.identity(); 68 | box.setAttribute("physx-body", "") 69 | 70 | }, 71 | 72 | remove() { 73 | if (this.timeout) { 74 | clearInterval(this.timeout) 75 | } 76 | } 77 | }); 78 | -------------------------------------------------------------------------------- /examples/compound/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Examples • Compound PhysX 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 |

Compound shape, using the PhysX driver.

18 |

Click when the red reticle is over the sphere to apply a force to it.

19 |
20 | 23 | view code 24 | 25 | 26 | 27 | 28 | 33 | 34 | 36 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 53 | 54 | 61 | 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /examples/constraints/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | A-Frame: Physics 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 |

Demonstration of many PhysX constraints including Fixed, Revolute, Spherical, 17 | Prismatic and D6 constraints.

18 |

Click when the red reticle is over a red object to apply a force to it.

19 |
20 | 23 | view code 24 | 25 | 27 | 28 | 29 | 34 | 35 | 36 | 37 | 41 | 42 | 43 | 44 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 59 | 60 | 64 | 66 | 67 | 68 | 69 | 70 | 71 | 78 | 79 | 80 | 84 | 91 | 95 | 96 | 97 | 99 | 103 | 104 | 105 | 106 | 107 | 108 | 115 | 116 | 123 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 139 | 140 | 147 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 161 | 162 | 167 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 185 | 186 | 193 | 199 | 200 | 201 | 202 | 203 | 204 | -------------------------------------------------------------------------------- /examples/materials/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Examples • Materials PhysX 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 |

Bounce simulation with restitution (bounciness) of 1 using the PhysX driver.

17 |
18 | 21 | view code 22 | 23 | 26 | 27 | 32 | 33 | 34 | 38 | 39 | 40 | 41 | 45 | 46 | 47 | 48 | 49 | 52 | 53 | 54 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /examples/pinboard/ammo-vs-physx.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 |

Comparison of performance between PhysX (left) and aframe-physics-system Ammo driver (right)

9 |
10 | 11 | 13 | 15 | 16 | -------------------------------------------------------------------------------- /examples/pinboard/ammo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Physics Benchmark Test - Ammo 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 17 | view code 18 | 19 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /examples/pinboard/physx.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Physics Benchmark Test - PhysX 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 16 | view code 17 | 18 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /examples/pinboard/pinboard.js: -------------------------------------------------------------------------------- 1 | // creates a pinboard height x width, + 2m at top & bottom, laid flat. 2 | AFRAME.registerComponent('pinboard', { 3 | 4 | schema: { 5 | physics: {type: 'string'}, // physx , ammo or cannon 6 | width: {type: 'number', default: 10}, 7 | height: {type: 'number', default: 10}, 8 | }, 9 | 10 | init() { 11 | 12 | const width = this.data.width; 13 | const height = this.data.height; 14 | 15 | const createBox = (width, height, depth, x, y, z, color, yRot) => { 16 | const box = document.createElement('a-box'); 17 | box.setAttribute('width', width) 18 | box.setAttribute('height', height) 19 | box.setAttribute('depth', depth) 20 | box.setAttribute('color', color) 21 | 22 | if (this.data.physics === "ammo") { 23 | box.setAttribute('ammo-body', 'type:static') 24 | box.setAttribute('ammo-shape', 'type:box;fit:all') 25 | } 26 | else if (this.data.physics === "cannon") { 27 | box.setAttribute('static-body', '') 28 | } 29 | else { 30 | box.setAttribute('physx-body', 'type:static') 31 | } 32 | 33 | box.object3D.position.set(x, y, z) 34 | box.object3D.rotation.set(0, yRot, 0) 35 | this.el.appendChild(box) 36 | 37 | return box 38 | } 39 | 40 | this.base = createBox(width, 1, height + 4, 0, -0.5, 0, 'grey', 0) 41 | createBox(0.1, 2, height + 4, width / 2 + 0.05, 0, 0, 'grey', 0) 42 | createBox(0.1, 2, height + 4, -width / 2 - 0.05, 0, 0, 'grey', 0) 43 | 44 | for (let ii = 0; ii < this.data.height; ii++) { 45 | const even = ii % 2 46 | for (let jj = 0; jj < this.data.width - 1; jj++) { 47 | 48 | createBox(0.1, 1, 0.1, 49 | jj - width / 2 + even / 2 + 0.75, 50 | 0.5, 51 | ii - height / 2 + 0.5, 52 | 'black', 53 | Math.PI / 4) 54 | } 55 | } 56 | } 57 | }) 58 | 59 | AFRAME.registerComponent('dynamic-ball', { 60 | 61 | schema: { 62 | physics: {type: 'string'}, // physx or ammo. 63 | yKill: {type: 'number', default: -10} 64 | }, 65 | 66 | init() { 67 | const el = this.el 68 | // Set geometry rather than radius - see: https://github.com/aframevr/aframe/issues/5203 69 | el.setAttribute('geometry', 'radius: 0.3') 70 | 71 | if (this.data.physics === "ammo") { 72 | el.setAttribute('ammo-body', 'type:dynamic') 73 | el.setAttribute('ammo-shape', 'type:sphere; fit:all') 74 | } 75 | else if (this.data.physics === "cannon") { 76 | // necessary to explicitly specify sphere radius, as async call to 77 | // set radius attribute on el may not have completed yet, and Cannon uses 78 | // the default radius of 1. 79 | // This is seen when recycling balls (deleting and recreating them). 80 | el.setAttribute('dynamic-body', 'shape: sphere; sphereRadius: 0.3') 81 | } 82 | else { 83 | el.setAttribute('physx-body', 'type:dynamic') 84 | } 85 | 86 | // Set material rather than color - see: https://github.com/aframevr/aframe/issues/5203 87 | el.setAttribute('material', 'color: yellow') 88 | }, 89 | 90 | tick() { 91 | if (this.el.object3D.position.y < this.data.yKill) { 92 | this.el.emit("recycle") 93 | } 94 | } 95 | }) 96 | 97 | AFRAME.registerComponent('ball-recycler', { 98 | 99 | schema: { 100 | physics: {type: 'string'}, // physx, ammo or cannon. 101 | ballCount: {type: 'number', default: 10}, 102 | width: {type: 'number', default: 8}, // width of spawn field 103 | depth: {type: 'number', default: 8}, // depth of spawn field (after initial spawn balls always spawned at far depth) 104 | yKill: {type: 'number', default: -10} 105 | }, 106 | 107 | init() { 108 | 109 | this.recycleBall = this.recycleBall.bind(this); 110 | 111 | // at start of day, spawn balls 112 | for (let ii = 0; ii < this.data.ballCount; ii++) { 113 | 114 | this.createBall(false) 115 | } 116 | }, 117 | 118 | createBall(recycled) { 119 | 120 | const { height, depth, width } = this.data 121 | const pos = this.el.object3D.position 122 | 123 | const ball = document.createElement('a-sphere') 124 | 125 | ball.setAttribute('dynamic-ball', {yKill: this.data.yKill, 126 | physics: this.data.physics}) 127 | x = pos.x + Math.random() * width - width / 2 128 | z = recycled ? (pos.z -depth / 2) : (pos.z + Math.random() * depth - depth / 2) 129 | ball.object3D.position.set(x, pos.y, z) 130 | this.el.sceneEl.appendChild(ball) 131 | 132 | ball.addEventListener('recycle', this.recycleBall); 133 | 134 | }, 135 | 136 | recycleBall(evt) { 137 | 138 | const ball = evt.target 139 | 140 | ball.parentNode.removeChild(ball); 141 | this.createBall(true) 142 | 143 | } 144 | }) 145 | 146 | 147 | AFRAME.registerComponent('tick-time-display', { 148 | 149 | schema: { 150 | outputEl: {type: 'selector'}, 151 | sceneOutputEl: {type: 'selector'} 152 | }, 153 | 154 | init() { 155 | this.updateStatsData = this.updateStatsData.bind(this); 156 | 157 | this.el.sceneEl.addEventListener('physics-tick-summary', this.updateStatsData) 158 | 159 | this.blankData = { 160 | percentile__50: "0.00", 161 | percentile__90: "0.00", 162 | max: "0.00" 163 | } 164 | }, 165 | 166 | updateStatsData(evt) { 167 | 168 | // Cover the fact that some engines (PhysX) don't output "before" data 169 | if (!evt.detail.before) { 170 | evt.detail.before = this.blankData 171 | } 172 | 173 | if (this.data.outputEl) { 174 | this.data.outputEl.innerHTML = `Engine: ${evt.detail.engine.percentile__50}
175 | Before: ${evt.detail.before.percentile__50}
176 | After: ${evt.detail.after.percentile__50}
177 | Total: ${evt.detail.total.percentile__50}` 178 | } 179 | 180 | if (this.data.sceneOutputEl) { 181 | const d = evt.detail 182 | this.data.sceneOutputEl.setAttribute("text", `value: Physics Tick Length (msecs) (over 100 ticks)\n--------------- Median --- 90th % --- Max -- 183 | Before: \t${d.before.percentile__50.padStart(7, ' ')}\t${d.before.percentile__90.padStart(7, ' ')}\t${d.before.max.padStart(7, ' ')} 184 | After: \t${d.after.percentile__50.padStart(7, ' ')}\t${d.after.percentile__90.padStart(7, ' ')}\t${d.after.max.padStart(7, ' ')} 185 | Engine: \t${d.engine.percentile__50.padStart(7, ' ')}\t${d.engine.percentile__90.padStart(7, ' ')}\t${d.engine.max.padStart(7, ' ')} 186 | Total: \t${d.total.percentile__50.padStart(7, ' ')}\t${d.total.percentile__90.padStart(7, ' ')}\t${d.total.max.padStart(7, ' ')}`) 187 | } 188 | } 189 | }) -------------------------------------------------------------------------------- /examples/sandbox/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Examples • PhysX 5 | 6 | 7 | 8 | 9 | 55 | 56 | 57 |
58 |

Demonstration of many PhysX driver features in a single example.

59 |

The cone is a kinematic object, and the purple box has a constraint attaching it to the cone.

60 |

Seems to be a problem with the torus scaling up/down - collision mesh does not seem to adjust.

61 |
62 | 65 | view code 66 | 67 | 68 | 69 | 77 | 85 | 92 | 99 | 100 | 101 | 102 | 111 | 119 | 129 | 138 | 147 | 148 | 149 | 150 | -------------------------------------------------------------------------------- /examples/spring/index.html: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | Examples • Spring PhysX 7 | 8 | 9 | 10 | 11 | 12 |
13 |

Four vertical springs each between two boxes with an 14 | assortment of damping and stiffness values using PhysX.

15 |
16 | 19 | view code 20 | 21 | 22 | 23 | 24 | 27 | 32 | 33 | 34 | 35 | 36 | 37 | 41 | 46 | 47 | 48 | 49 | 50 | 51 | 55 | 60 | 61 | 62 | 63 | 64 | 65 | 69 | 75 | 76 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /examples/stress/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | A-Frame: Physics 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 |

Point the red reticle at a block, and click the mouse to apply a strong force to it.

20 |

Use mouse and WASD to look and move around.

21 |
22 | 25 | view code 26 | 27 | 30 | 31 | 32 | 33 | 38 | 39 | 40 | 41 | 45 | 46 | 47 | 48 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | -------------------------------------------------------------------------------- /examples/sweeper/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Examples • Sweeper PhysX 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 |

Blocks fall at random from the sky. The sweeper wall sweeps them away.

18 |
19 | 22 | view code 23 | 24 | 27 | 29 | 30 | 31 | 36 | 37 | 39 | 41 | 42 | 43 | 44 | 45 | 46 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /examples/ttl/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Examples • TTL PhysX 5 | 6 | 7 | 8 | 9 | 28 | 29 | 30 |
31 |

The box disappears after 100 frames.

32 |
33 | 36 | view code 37 | 38 | 39 | 40 | 41 | 42 | 44 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | require('./src/physics'); 2 | require('aframe-stats-panel'); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@c-frame/physx", 3 | "version": "0.2.0", 4 | "description": "Physics for A-Frame using Nvidia PhysX", 5 | "main": "index.js", 6 | "directories": { 7 | "example": "examples" 8 | }, 9 | "scripts": { 10 | "start": "npm run dev", 11 | "dev": "npx webpack serve", 12 | "dist": "npm run dist:dev && npm run dist:prod", 13 | "dist:dev": "webpack --config webpack.config.js", 14 | "dist:prod": "webpack --config webpack.prod.config.js", 15 | "test": "echo \"Error: no test specified\" && exit 1" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "git+https://github.com/c-frame/physx.git" 20 | }, 21 | "keywords": [ 22 | "physics", 23 | "A-Frame", 24 | "aframe", 25 | "aframe-component", 26 | "threejs", 27 | "webXR", 28 | "physx" 29 | ], 30 | "author": "Diarmid Mackenzie", 31 | "license": "MIT", 32 | "bugs": { 33 | "url": "https://github.com/c-frame/physx/issues" 34 | }, 35 | "homepage": "https://github.com/c-frame/physx#readme", 36 | "dependencies": { 37 | "aframe-stats-panel": "^0.2.3" 38 | }, 39 | "devDependencies": { 40 | "webpack": "^5.75.0", 41 | "webpack-cli": "^4.10.0", 42 | "webpack-dev-server": "^4.11.1", 43 | "webpack-merge": "^5.8.0" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/physics.js: -------------------------------------------------------------------------------- 1 | // This is a modification of the physics/PhysX libraries 2 | // created by Lee Stemkoski 3 | // from the VARTISTE project @ https://vartiste.xyz/ 4 | // by Zachary Capalbo https://github.com/zach-capalbo/vartiste 5 | // with the goal of creating a simplified standalone codebase. 6 | // Further performance modifications by Diarmid Mackenzie. 7 | 8 | // original documentation: https://vartiste.xyz/docs.html#physics.js 9 | 10 | // Came via: https://github.com/stemkoski/A-Frame-Examples/blob/66f05fe5cf89879996f1f6a4c0475ce475e8796a/js/physics.js 11 | // and then via: https://github.com/diarmidmackenzie/christmas-scene/blob/a94ae7e7167937f10d34df8429fb71641e343bb1/lib/physics.js 12 | // ====================================================================== 13 | 14 | let PHYSX = require('./physx.release.js'); 15 | 16 | // patching in Pool functions 17 | var poolSize = 0 18 | 19 | function sysPool(name, type) { 20 | if (this.system._pool[name]) return this.system._pool[name] 21 | this.system._pool[name] = new type() 22 | // console.log("SysPooling", type.name) 23 | return this.system._pool[name] 24 | } 25 | 26 | function pool(name, type) { 27 | if (this._pool[name]) return this._pool[name] 28 | this._pool[name] = new type() 29 | // console.log("Pooling", type.name) 30 | return this._pool[name] 31 | } 32 | 33 | class Pool { 34 | static init(where, {useSystem = false} = {}) { 35 | if (useSystem) 36 | { 37 | if (!where.system) { 38 | console.error("No system for system pool", where.attrName) 39 | } 40 | if (!where.system._pool) where.system._pool = {}; 41 | 42 | where.pool = sysPool; 43 | } 44 | else 45 | { 46 | where._pool = {} 47 | where.pool = pool; 48 | } 49 | } 50 | } 51 | 52 | // ================================================================================================== 53 | 54 | // patching in required Util functions from VARTISTE 55 | 56 | Util = {} 57 | 58 | Pool.init(Util); 59 | 60 | // Copies `matrix` into `obj`'s (a `THREE.Object3D`) `matrix`, and decomposes 61 | // it to `obj`'s position, rotation, and scale 62 | Util.applyMatrix = function(matrix, obj) { 63 | obj.matrix.copy(matrix) 64 | matrix.decompose(obj.position, obj.rotation, obj.scale) 65 | } 66 | 67 | Util.traverseCondition = function(obj3D, condition, fn) 68 | { 69 | if (!condition(obj3D)) return; 70 | 71 | fn(obj3D) 72 | for (let c of obj3D.children) 73 | { 74 | this.traverseCondition(c, condition, fn) 75 | } 76 | } 77 | 78 | Util.positionObject3DAtTarget = function(obj, target, {scale, transformOffset, transformRoot} = {}) 79 | { 80 | if (typeof transformRoot === 'undefined') transformRoot = obj.parent 81 | 82 | target.updateWorldMatrix() 83 | let destMat = this.pool('dest', THREE.Matrix4) 84 | destMat.copy(target.matrixWorld) 85 | 86 | if (transformOffset) { 87 | let transformMat = this.pool('transformMat', THREE.Matrix4) 88 | transformMat.makeTranslation(transformOffset.x, transformOffset.y, transformOffset.z) 89 | destMat.multiply(transformMat) 90 | } 91 | 92 | if (scale) { 93 | let scaleVect = this.pool('scale', THREE.Vector3) 94 | scaleVect.setFromMatrixScale(destMat) 95 | scaleVect.set(scale.x / scaleVect.x, scale.y / scaleVect.y, scale.z / scaleVect.z) 96 | destMat.scale(scaleVect) 97 | } 98 | 99 | let invMat = this.pool('inv', THREE.Matrix4) 100 | 101 | transformRoot.updateWorldMatrix() 102 | invMat.copy(transformRoot.matrixWorld).invert() 103 | destMat.premultiply(invMat) 104 | 105 | Util.applyMatrix(destMat, obj) 106 | } 107 | 108 | // untested functions 109 | 110 | // Executes function `fn` when `entity` has finished loading, or immediately 111 | // if it has already loaded. `entity` may be a single `a-entity` element, or 112 | // an array of `a-entity` elements. If `fn` is not provided, it will return a 113 | // `Promise` that will resolve when `entity` is loaded (or immediately if 114 | // `entity` is already loaded). 115 | Util.whenLoaded = function(entity, fn) { 116 | if (Array.isArray(entity) && fn) return whenLoadedAll(entity, fn) 117 | if (Array.isArray(entity)) return awaitLoadingAll(entity) 118 | if (fn) return whenLoadedSingle(entity, fn) 119 | return awaitLoadingSingle(entity) 120 | } 121 | 122 | function whenLoadedSingle(entity, fn) { 123 | if (entity.hasLoaded) 124 | { 125 | fn() 126 | } 127 | else 128 | { 129 | entity.addEventListener('loaded', fn) 130 | } 131 | } 132 | 133 | function whenLoadedAll(entities, fn) { 134 | let allLoaded = entities.map(() => false) 135 | for (let i = 0; i < entities.length; ++i) 136 | { 137 | let ii = i 138 | let entity = entities[ii] 139 | whenLoadedSingle(entity, () => { 140 | allLoaded[ii] = true 141 | if (allLoaded.every(t => t)) fn() 142 | }) 143 | } 144 | } 145 | 146 | function awaitLoadingSingle(entity) { 147 | return new Promise((r, e) => whenLoadedSingle(entity, r)) 148 | } 149 | 150 | async function awaitLoadingAll(entities) { 151 | for (let entity of entities) 152 | { 153 | await awaitLoadingSingle(entity) 154 | } 155 | } 156 | 157 | Util.whenComponentInitialized = function(el, component, fn) { 158 | if (el && el.components[component] && el.components[component].initialized) { 159 | return Promise.resolve(fn ? fn() : undefined) 160 | } 161 | 162 | return new Promise((r, e) => { 163 | if (el && el.components[component] && el.components[component].initialized) { 164 | return Promise.resolve(fn ? fn() : undefined) 165 | } 166 | 167 | let listener = (e) => { 168 | if (e.detail.name === component) { 169 | el.removeEventListener('componentinitialized', listener); 170 | if (fn) fn(); 171 | r(); 172 | } 173 | }; 174 | el.addEventListener('componentinitialized', listener) 175 | }) 176 | } 177 | 178 | // ======================================================================================== 179 | 180 | // Extra utility functions for dealing with PhysX 181 | 182 | const PhysXUtil = { 183 | // Gets the world position transform of the given object3D in PhysX format 184 | object3DPhysXTransform: (() => { 185 | let pos = new THREE.Vector3(); 186 | let quat = new THREE.Quaternion(); 187 | return function (obj) { 188 | obj.getWorldPosition(pos); 189 | obj.getWorldQuaternion(quat); 190 | 191 | return { 192 | translation: { 193 | x: pos.x, 194 | y: pos.y, 195 | z: pos.z, 196 | }, 197 | rotation: { 198 | w: quat.w, // PhysX uses WXYZ quaternions, 199 | x: quat.x, 200 | y: quat.y, 201 | z: quat.z, 202 | }, 203 | } 204 | } 205 | })(), 206 | 207 | // Converts a THREE.Matrix4 into a PhysX transform 208 | matrixToTransform: (() => { 209 | let pos = new THREE.Vector3(); 210 | let quat = new THREE.Quaternion(); 211 | let scale = new THREE.Vector3(); 212 | let scaleInv = new THREE.Matrix4(); 213 | let mat2 = new THREE.Matrix4(); 214 | return function (matrix) { 215 | matrix.decompose(pos, quat, scale); 216 | 217 | return { 218 | translation: { 219 | x: pos.x, 220 | y: pos.y, 221 | z: pos.z, 222 | }, 223 | rotation: { 224 | w: quat.w, // PhysX uses WXYZ quaternions, 225 | x: quat.x, 226 | y: quat.y, 227 | z: quat.z, 228 | }, 229 | } 230 | } 231 | })(), 232 | 233 | // Converts an arry of layer numbers to an integer bitmask 234 | layersToMask: (() => { 235 | let layers = new THREE.Layers(); 236 | return function(layerArray) { 237 | layers.disableAll(); 238 | for (let layer of layerArray) 239 | { 240 | layers.enable(parseInt(layer)); 241 | } 242 | return layers.mask; 243 | }; 244 | })(), 245 | 246 | axisArrayToEnums: function(axes) { 247 | let enumAxes = [] 248 | for (let axis of axes) 249 | { 250 | if (axis === 'swing') { 251 | enumAxes.push(PhysX.PxD6Axis.eSWING1) 252 | enumAxes.push(PhysX.PxD6Axis.eSWING2) 253 | continue 254 | } 255 | let enumKey = `e${axis.toUpperCase()}` 256 | if (!(enumKey in PhysX.PxD6Axis)) 257 | { 258 | console.warn(`Unknown axis ${axis} (PxD6Axis::${enumKey})`) 259 | } 260 | enumAxes.push(PhysX.PxD6Axis[enumKey]) 261 | } 262 | return enumAxes; 263 | } 264 | }; 265 | 266 | let PhysX 267 | 268 | // Implements the a physics system using an emscripten compiled PhysX engine. 269 | // 270 | // 271 | // If `autoLoad` is `true`, or when you call `startPhysX`, the `physx` system will 272 | // automatically load and initialize the physics system with reasonable defaults 273 | // and a ground plane. All you have to do is add [`physx-body`](#physx-body) to 274 | // the bodies that you want to be part of the simulation. The system will take 275 | // try to take care of things like collision meshes, position updates, etc 276 | // automatically. The simplest physics scene looks something like: 277 | // 278 | //``` 279 | // 280 | // 281 | // 282 | // 283 | // 284 | // 285 | // 286 | //``` 287 | // 288 | // If you want a little more control over how things behave, you can set the 289 | // [`physx-material`](#physx-material) component on the objects in your 290 | // simulation, or use [`physx-joint`s](#physx-joint), 291 | // [`physx-constraint`s](#physx-constraint) and [`physx-driver`s](#physx-driver) 292 | // to add some complexity to your scene. 293 | // 294 | // If you need more low-level control, the PhysX bindings are exposed through 295 | // the `PhysX` property of the system. So for instance, if you wanted to make 296 | // use of the [`PxCapsuleGeometry`](https://gameworksdocs.nvidia.com/PhysX/4.1/documentation/physxapi/files/classPxCapsuleGeometry.html) 297 | // in your own component, you would call: 298 | // 299 | //``` 300 | // let myGeometry = new this.el.sceneEl.PhysX.PxCapsuleGeometry(1.0, 2.0) 301 | //``` 302 | // 303 | // The system uses [my fork](https://github.com/zach-capalbo/PhysX) of PhysX, built using the [Docker Wrapper](https://github.com/ashconnell/physx-js). To see what's exposed to JavaScript, see [PxWebBindings.cpp](https://github.com/zach-capalbo/PhysX/blob/emscripten_wip/physx/source/physxwebbindings/src/PxWebBindings.cpp) 304 | // 305 | // For a complete example of how to use this, you can see the 306 | // [aframe-vartiste-toolkit Physics 307 | // Playground](https://glitch.com/edit/#!/fascinated-hip-period?path=index.html) 308 | // 309 | // It is also helpful to refer to the [NVIDIA PhysX 310 | // documentation](https://nvidiagameworks.github.io/PhysX/4.1/documentation/physxguide/Index.html) 311 | AFRAME.registerSystem('physx', { 312 | schema: { 313 | // Amount of time to wait after loading before starting the physics. Can be 314 | // useful if there is still some things loading or initializing elsewhere in 315 | // the scene 316 | delay: {default: 5000}, 317 | 318 | // Throttle for running the physics simulation. On complex scenes, you can 319 | // increase this to avoid dropping video frames 320 | throttle: {default: 10}, 321 | 322 | // If true, the PhysX will automatically be loaded and started. If false, 323 | // you will have to call `startPhysX()` manually to load and start the 324 | // physics engine 325 | autoLoad: {default: false}, 326 | 327 | // Simulation speed multiplier. Increase or decrease to speed up or slow 328 | // down simulation time 329 | speed: {default: 1.0}, 330 | 331 | // URL for the PhysX WASM bundle. 332 | wasmUrl: {default: "../../wasm/physx.release.wasm"}, 333 | 334 | // If true, sets up a default scene with a ground plane and bounding 335 | // cylinder. 336 | useDefaultScene: {default: true}, 337 | 338 | // NYI 339 | wrapBounds: {default: false}, 340 | 341 | // Which collision layers the ground belongs to 342 | groundCollisionLayers: {default: [2]}, 343 | 344 | // Which collision layers will collide with the ground 345 | groundCollisionMask: {default: [1,2,3,4]}, 346 | 347 | // Global gravity vector 348 | gravity: {type: 'vec3', default: {x: 0, y: -9.8, z: 0}}, 349 | 350 | // Whether to output stats, and how to output them. One or more of "console", "events", "panel" 351 | stats: {type: 'array', default: []} 352 | }, 353 | init() { 354 | this.PhysXUtil = PhysXUtil; 355 | 356 | // for logging. 357 | this.cumTimeEngine = 0; 358 | this.cumTimeWrapper = 0; 359 | this.tickCounter = 0; 360 | 361 | 362 | this.objects = new Map(); 363 | this.shapeMap = new Map(); 364 | this.jointMap = new Map(); 365 | this.boundaryShapes = new Set(); 366 | this.worldHelper = new THREE.Object3D(); 367 | this.el.object3D.add(this.worldHelper); 368 | this.tock = AFRAME.utils.throttleTick(this.tock, this.data.throttle, this) 369 | this.collisionObject = {thisShape: null, otherShape:null, points: [], impulses: [], otherComponent: null}; 370 | 371 | let defaultTarget = document.createElement('a-entity') 372 | this.el.append(defaultTarget) 373 | this.defaultTarget = defaultTarget 374 | 375 | this.initializePhysX = new Promise((r, e) => { 376 | this.fulfillPhysXPromise = r; 377 | }) 378 | 379 | this.initStats() 380 | 381 | this.el.addEventListener('inspectortoggle', (e) => { 382 | console.log("Inspector toggle", e) 383 | if (e.detail === true) 384 | { 385 | this.running = false 386 | } 387 | }) 388 | }, 389 | 390 | initStats() { 391 | // Data used for performance monitoring. 392 | this.statsToConsole = this.data.stats.includes("console") 393 | this.statsToEvents = this.data.stats.includes("events") 394 | this.statsToPanel = this.data.stats.includes("panel") 395 | 396 | this.bodyTypeToStatsPropertyMap = { 397 | "static": "staticBodies", 398 | "dynamic": "dynamicBodies", 399 | "kinematic": "kinematicBodies", 400 | } 401 | 402 | if (this.statsToConsole || this.statsToEvents || this.statsToPanel) { 403 | this.trackPerf = true; 404 | this.tickCounter = 0; 405 | this.statsTickData = {}; 406 | this.statsBodyData = {}; 407 | 408 | const scene = this.el.sceneEl; 409 | scene.setAttribute("stats-collector", `inEvent: physics-tick-data; 410 | properties: engine, after, total; 411 | outputFrequency: 100; 412 | outEvent: physics-tick-summary; 413 | outputs: percentile__50, percentile__90, max`); 414 | } 415 | 416 | if (this.statsToPanel) { 417 | const scene = this.el.sceneEl; 418 | const space = "   " 419 | 420 | scene.setAttribute("stats-panel", "") 421 | scene.setAttribute("stats-group__bodies", `label: Physics Bodies`) 422 | scene.setAttribute("stats-row__b1", `group: bodies; 423 | event:physics-body-data; 424 | properties: staticBodies; 425 | label: Static`) 426 | scene.setAttribute("stats-row__b2", `group: bodies; 427 | event:physics-body-data; 428 | properties: dynamicBodies; 429 | label: Dynamic`) 430 | scene.setAttribute("stats-row__b3", `group: bodies; 431 | event:physics-body-data; 432 | properties: kinematicBodies; 433 | label: Kinematic`) 434 | scene.setAttribute("stats-group__tick", `label: Physics Ticks: Median${space}90th%${space}99th%`) 435 | scene.setAttribute("stats-row__1", `group: tick; 436 | event:physics-tick-summary; 437 | properties: engine.percentile__50, 438 | engine.percentile__90, 439 | engine.max; 440 | label: Engine`) 441 | scene.setAttribute("stats-row__2", `group: tick; 442 | event:physics-tick-summary; 443 | properties: after.percentile__50, 444 | after.percentile__90, 445 | after.max; 446 | label: After`) 447 | 448 | scene.setAttribute("stats-row__3", `group: tick; 449 | event:physics-tick-summary; 450 | properties: total.percentile__50, 451 | total.percentile__90, 452 | total.max; 453 | label: Total`) 454 | } 455 | }, 456 | findWasm() { 457 | return this.data.wasmUrl; 458 | }, 459 | // Loads PhysX and starts the simulation 460 | async startPhysX() { 461 | this.running = true; 462 | let self = this; 463 | let resolveInitialized; 464 | let initialized = new Promise((r, e) => resolveInitialized = r) 465 | let instance = PHYSX({ 466 | locateFile() { 467 | return self.findWasm() 468 | }, 469 | onRuntimeInitialized() { 470 | resolveInitialized(); 471 | } 472 | }); 473 | if (instance instanceof Promise) instance = await instance; 474 | this.PhysX = instance; 475 | PhysX = instance; 476 | await initialized; 477 | self.startPhysXScene() 478 | self.physXInitialized = true 479 | self.fulfillPhysXPromise() 480 | self.el.emit('physx-started', {}) 481 | }, 482 | startPhysXScene() { 483 | console.info("Starting PhysX scene") 484 | const foundation = PhysX.PxCreateFoundation( 485 | PhysX.PX_PHYSICS_VERSION, 486 | new PhysX.PxDefaultAllocator(), 487 | new PhysX.PxDefaultErrorCallback() 488 | ); 489 | this.foundation = foundation 490 | const physxSimulationCallbackInstance = PhysX.PxSimulationEventCallback.implement({ 491 | onContactBegin: (shape0, shape1, points, impulses) => { 492 | let c0 = this.shapeMap.get(shape0.$$.ptr) 493 | let c1 = this.shapeMap.get(shape1.$$.ptr) 494 | 495 | if (c1 === c0) return; 496 | 497 | if (c0 && c0.data.emitCollisionEvents) { 498 | this.collisionObject.thisShape = shape0 499 | this.collisionObject.otherShape = shape1 500 | this.collisionObject.points = points 501 | this.collisionObject.impulses = impulses 502 | this.collisionObject.otherComponent = c1 503 | c0.el.emit('contactbegin', this.collisionObject) 504 | } 505 | 506 | if (c1 && c1.data.emitCollisionEvents) { 507 | this.collisionObject.thisShape = shape1 508 | this.collisionObject.otherShape = shape0 509 | this.collisionObject.points = points 510 | this.collisionObject.impulses = impulses 511 | this.collisionObject.otherComponent = c0 512 | c1.el.emit('contactbegin', this.collisionObject) 513 | } 514 | }, 515 | onContactEnd: (shape0, shape1) => { 516 | let c0 = this.shapeMap.get(shape0.$$.ptr) 517 | let c1 = this.shapeMap.get(shape1.$$.ptr) 518 | 519 | if (c1 === c0) return; 520 | 521 | if (c0 && c0.data.emitCollisionEvents) { 522 | this.collisionObject.thisShape = shape0 523 | this.collisionObject.otherShape = shape1 524 | this.collisionObject.points = null 525 | this.collisionObject.impulses = null 526 | this.collisionObject.otherComponent = c1 527 | c0.el.emit('contactend', this.collisionObject) 528 | } 529 | 530 | if (c1 && c1.data.emitCollisionEvents) { 531 | this.collisionObject.thisShape = shape1 532 | this.collisionObject.otherShape = shape0 533 | this.collisionObject.points = null 534 | this.collisionObject.impulses = null 535 | this.collisionObject.otherComponent = c0 536 | c1.el.emit('contactend', this.collisionObject) 537 | } 538 | }, 539 | onContactPersist: () => {}, 540 | onTriggerBegin: () => {}, 541 | onTriggerEnd: () => {}, 542 | onConstraintBreak: (joint) => { 543 | let component = this.jointMap.get(joint.$$.ptr); 544 | 545 | if (!component) return; 546 | 547 | component.el.emit('constraintbreak', {}) 548 | }, 549 | }); 550 | let tolerance = new PhysX.PxTolerancesScale(); 551 | // tolerance.length /= 10; 552 | // console.log("Tolerances", tolerance.length, tolerance.speed); 553 | this.physics = PhysX.PxCreatePhysics( 554 | PhysX.PX_PHYSICS_VERSION, 555 | foundation, 556 | tolerance, 557 | false, 558 | null 559 | ) 560 | PhysX.PxInitExtensions(this.physics, null); 561 | 562 | this.cooking = PhysX.PxCreateCooking( 563 | PhysX.PX_PHYSICS_VERSION, 564 | foundation, 565 | new PhysX.PxCookingParams(tolerance) 566 | ) 567 | 568 | const sceneDesc = PhysX.getDefaultSceneDesc( 569 | this.physics.getTolerancesScale(), 570 | 0, 571 | physxSimulationCallbackInstance 572 | ) 573 | this.scene = this.physics.createScene(sceneDesc) 574 | 575 | this.setupDefaultEnvironment() 576 | }, 577 | setupDefaultEnvironment() { 578 | this.defaultActorFlags = new PhysX.PxShapeFlags( 579 | PhysX.PxShapeFlag.eSCENE_QUERY_SHAPE.value | 580 | PhysX.PxShapeFlag.eSIMULATION_SHAPE.value 581 | ) 582 | this.defaultFilterData = new PhysX.PxFilterData(PhysXUtil.layersToMask(this.data.groundCollisionLayers), PhysXUtil.layersToMask(this.data.groundCollisionMask), 0, 0); 583 | 584 | this.scene.setGravity(this.data.gravity) 585 | 586 | if (this.data.useDefaultScene) 587 | { 588 | this.createGroundPlane() 589 | this.createBoundingCylinder() 590 | } 591 | 592 | 593 | this.defaultTarget.setAttribute('physx-body', 'type', 'static') 594 | 595 | }, 596 | createGroundPlane() { 597 | let geometry = new PhysX.PxPlaneGeometry(); 598 | // let geometry = new PhysX.PxBoxGeometry(10, 1, 10); 599 | let material = this.physics.createMaterial(0.8, 0.8, 0.1); 600 | 601 | const shape = this.physics.createShape(geometry, material, false, this.defaultActorFlags) 602 | shape.setQueryFilterData(this.defaultFilterData) 603 | shape.setSimulationFilterData(this.defaultFilterData) 604 | const transform = { 605 | translation: { 606 | x: 0, 607 | y: 0, 608 | z: -5, 609 | }, 610 | rotation: { 611 | w: 0.707107, // PhysX uses WXYZ quaternions, 612 | x: 0, 613 | y: 0, 614 | z: 0.707107, 615 | }, 616 | } 617 | let body = this.physics.createRigidStatic(transform) 618 | body.attachShape(shape) 619 | this.scene.addActor(body, null) 620 | this.ground = body 621 | this.rigidBody = body 622 | }, 623 | createBoundingCylinder() { 624 | const numPlanes = 16 625 | let geometry = new PhysX.PxPlaneGeometry(); 626 | let material = this.physics.createMaterial(0.1, 0.1, 0.8); 627 | let spherical = new THREE.Spherical(); 628 | spherical.radius = 30; 629 | let quat = new THREE.Quaternion(); 630 | let pos = new THREE.Vector3; 631 | let euler = new THREE.Euler(); 632 | 633 | for (let i = 0; i < numPlanes; ++i) 634 | { 635 | spherical.theta = i * 2.0 * Math.PI / numPlanes; 636 | pos.setFromSphericalCoords(spherical.radius, spherical.theta, spherical.phi) 637 | pos.x = - pos.y 638 | pos.y = 0; 639 | euler.set(0, spherical.theta, 0); 640 | quat.setFromEuler(euler) 641 | 642 | const shape = this.physics.createShape(geometry, material, false, this.defaultActorFlags) 643 | shape.setQueryFilterData(this.defaultFilterData) 644 | shape.setSimulationFilterData(this.defaultFilterData) 645 | const transform = { 646 | translation: { 647 | x: pos.x, 648 | y: pos.y, 649 | z: pos.z, 650 | }, 651 | rotation: { 652 | w: quat.w, // PhysX uses WXYZ quaternions, 653 | x: quat.x, 654 | y: quat.y, 655 | z: quat.z, 656 | }, 657 | } 658 | this.boundaryShapes.add(shape.$$.ptr) 659 | let body = this.physics.createRigidStatic(transform) 660 | body.attachShape(shape) 661 | this.scene.addActor(body, null) 662 | } 663 | }, 664 | async registerComponentBody(component, {type}) { 665 | await this.initializePhysX; 666 | 667 | // const shape = this.physics.createShape(geometry, material, false, flags) 668 | const transform = PhysXUtil.object3DPhysXTransform(component.el.object3D); 669 | 670 | let body 671 | if (type === 'dynamic' || type === 'kinematic') 672 | { 673 | body = this.physics.createRigidDynamic(transform) 674 | 675 | // body.setRigidBodyFlag(PhysX.PxRigidBodyFlag.eENABLE_CCD, true); 676 | // body.setMaxContactImpulse(1e2); 677 | } 678 | else 679 | { 680 | body = this.physics.createRigidStatic(transform) 681 | } 682 | 683 | let attemptToUseDensity = true; 684 | let seenAnyDensity = false; 685 | let densities = new PhysX.VectorPxReal() 686 | for (let shape of component.createShapes(this.physics, this.defaultActorFlags)) 687 | { 688 | body.attachShape(shape) 689 | 690 | if (isFinite(shape.density)) 691 | { 692 | seenAnyDensity = true 693 | densities.push_back(shape.density) 694 | } 695 | else 696 | { 697 | attemptToUseDensity = false 698 | 699 | if (seenAnyDensity) 700 | { 701 | console.warn("Densities not set for all shapes. Will use total mass instead.", component.el) 702 | } 703 | } 704 | } 705 | if (type === 'dynamic' || type === 'kinematic') { 706 | if (attemptToUseDensity && seenAnyDensity) 707 | { 708 | console.log("Setting density vector", densities) 709 | body.updateMassAndInertia(densities) 710 | } 711 | else { 712 | body.setMassAndUpdateInertia(component.data.mass) 713 | } 714 | } 715 | densities.delete() 716 | this.scene.addActor(body, null) 717 | this.objects.set(component.el.object3D, body) 718 | component.rigidBody = body 719 | }, 720 | registerShape(shape, component) { 721 | this.shapeMap.set(shape.$$.ptr, component); 722 | }, 723 | registerJoint(joint, component) { 724 | this.jointMap.set(joint.$$.ptr, component); 725 | }, 726 | removeBody(component) { 727 | let body = component.rigidBody 728 | this.objects.delete(component.el.object3D) 729 | body.release() 730 | }, 731 | tock(t, dt) { 732 | if (t < this.data.delay) return 733 | if (!this.physXInitialized && this.data.autoLoad && !this.running) this.startPhysX() 734 | if (!this.physXInitialized) return 735 | if (!this.running) return 736 | 737 | const engineStartTime = performance.now(); 738 | 739 | this.scene.simulate(THREE.MathUtils.clamp(dt * this.data.speed / 1000, 0, 0.03 * this.data.speed), true) 740 | //this.scene.simulate(0.02, true) // (experiment with fixed interval) 741 | this.scene.fetchResults(true) 742 | 743 | const engineEndTime = performance.now(); 744 | 745 | for (let [obj, body] of this.objects) 746 | { 747 | // no updates needed for static objects. 748 | if (obj.el.components['physx-body'].data.type === 'static') continue; 749 | 750 | const transform = body.getGlobalPose() 751 | this.worldHelper.position.copy(transform.translation); 752 | this.worldHelper.quaternion.copy(transform.rotation); 753 | obj.getWorldScale(this.worldHelper.scale) 754 | Util.positionObject3DAtTarget(obj, this.worldHelper); 755 | } 756 | 757 | if (this.trackPerf) { 758 | const afterEndTime = performance.now(); 759 | 760 | this.statsTickData.engine = engineEndTime - engineStartTime 761 | this.statsTickData.after = afterEndTime - engineEndTime 762 | this.statsTickData.total = afterEndTime - engineStartTime 763 | this.el.emit("physics-tick-data", this.statsTickData) 764 | 765 | this.tickCounter++; 766 | 767 | if (this.tickCounter === 100) { 768 | 769 | this.countBodies() 770 | 771 | if (this.statsToConsole) { 772 | console.log("Physics tick stats:", this.statsData) 773 | } 774 | 775 | if (this.statsToEvents || this.statsToPanel) { 776 | this.el.emit("physics-body-data", this.statsBodyData) 777 | } 778 | 779 | this.tickCounter = 0; 780 | } 781 | } 782 | }, 783 | 784 | countBodies() { 785 | 786 | // Aditional statistics beyond simple body counts should be possible. 787 | // They could be accessed via PxScene::getSimulationStatistics() 788 | // https://nvidiagameworks.github.io/PhysX/4.1/documentation/physxguide/Manual/Statistics.html 789 | // https://nvidiagameworks.github.io/PhysX/4.1/documentation/physxapi/files/classPxSimulationStatistics.html 790 | // However this part of the API is not yet exposed in the 791 | // WASM PhysX build we are using 792 | // See: https://github.com/zach-capalbo/PhysX/blob/emscripten_wip/physx/source/physxwebbindings/src/PxWebBindings.cpp 793 | 794 | const statsData = this.statsBodyData 795 | statsData.staticBodies = 0 796 | statsData.kinematicBodies = 0 797 | statsData.dynamicBodies = 0 798 | 799 | this.objects.forEach((pxBody, object3D) => { 800 | const el = object3D.el 801 | const type = el.components['physx-body'].data.type 802 | const property = this.bodyTypeToStatsPropertyMap[type] 803 | statsData[property]++ 804 | }) 805 | }, 806 | }) 807 | 808 | // Controls physics properties for individual shapes or rigid bodies. You can 809 | // set this either on an entity with the `phyx-body` component, or on a shape or 810 | // model contained in an entity with the `physx-body` component. If it's set on 811 | // a `physx-body`, it will be the default material for all shapes in that body. 812 | // If it's set on an element containing geometry or a model, it will be the 813 | // material used for that shape only. 814 | // 815 | // For instance, in the following scene fragment: 816 | //``` 817 | // 818 | // 819 | // 820 | // 821 | // 822 | //``` 823 | // 824 | // `shape1`, which is part of the `bodyA` rigid body, will have static friction 825 | // of 1.0, since it has a material set on it. `shape2`, which is also part of 826 | // the `bodyA` rigid body, will have a static friction of 0.5, since that is 827 | // the body default. `bodyB` will have the component default of 0.2, since it is 828 | // a separate body. 829 | AFRAME.registerComponent('physx-material', { 830 | schema: { 831 | // Static friction 832 | staticFriction: {default: 0.2}, 833 | // Dynamic friction 834 | dynamicFriction: {default: 0.2}, 835 | // Restitution, or "bounciness" 836 | restitution: {default: 0.2}, 837 | 838 | // Density for the shape. If densities are specified for _all_ shapes in a 839 | // rigid body, then the rigid body's mass properties will be automatically 840 | // calculated based on the different densities. However, if density 841 | // information is not specified for every shape, then the mass defined in 842 | // the overarching [`physx-body`](#physx-body) will be used instead. 843 | density: {type: 'number', default: NaN}, 844 | 845 | // Which collision layers this shape is present on 846 | collisionLayers: {default: [1], type: 'array'}, 847 | // Array containing all layers that this shape should collide with 848 | collidesWithLayers: {default: [1,2,3,4], type: 'array'}, 849 | 850 | // If `collisionGroup` is greater than 0, this shape will *not* collide with 851 | // any other shape with the same `collisionGroup` value 852 | collisionGroup: {default: 0}, 853 | 854 | // If >= 0, this will set the PhysX contact offset, indicating how far away 855 | // from the shape simulation contact events should begin. 856 | contactOffset: {default: -1.0}, 857 | 858 | // If >= 0, this will set the PhysX rest offset 859 | restOffset: {default: -1.0}, 860 | } 861 | }) 862 | 863 | // Turns an entity into a PhysX rigid body. This is the main component for 864 | // creating physics objects. 865 | // 866 | // **Types** 867 | // 868 | // There are 3 types of supported rigid bodies. The type can be set by using the 869 | // `type` proeprty, but once initialized cannot be changed. 870 | // 871 | // - `dynamic` objects are objects that will have physics simulated on them. The 872 | // entity's world position, scale, and rotation will be used as the starting 873 | // condition for the simulation, however once the simulation starts the 874 | // entity's position and rotation will be replaced each frame with the results 875 | // of the simulation. 876 | // - `static` objects are objects that cannot move. They cab be used to create 877 | // collidable objects for `dynamic` objects, or for anchor points for joints. 878 | // - `kinematic` objects are objects that can be moved programmatically, but 879 | // will not be moved by the simulation. They can however, interact with and 880 | // collide with dynamic objects. Each frame, the entity's `object3D` will be 881 | // used to set the position and rotation for the simulation object. 882 | // 883 | // **Shapes** 884 | // 885 | // When the component is initialized, and on the `object3dset` event, all 886 | // visible meshes that are descendents of this entity will have shapes created 887 | // for them. Each individual mesh will have its own convex hull automatically 888 | // generated for it. This means you can have reasonably accurate collision 889 | // meshes both from building up shapes with a-frame geometry primitives, and 890 | // from importing 3D models. 891 | // 892 | // Visible meshes can be excluded from this shape generation process by setting 893 | // the `physx-no-collision` attribute on the corresponding `a-entity` element. 894 | // Invisible meshes can be included into this shape generation process by 895 | // settingt the `physx-hidden-collision` attribute on the corresponding 896 | // `a-entity` element. This can be especially useful when using an external tool 897 | // (like [Blender V-HACD](https://github.com/andyp123/blender_vhacd)) to create 898 | // a low-poly convex collision mesh for a high-poly or concave mesh. This leads 899 | // to this pattern for such cases: 900 | // 901 | // ``` 902 | // 903 | // 904 | // 905 | // 906 | // ``` 907 | // 908 | // Note, in such cases that if you are setting material properties on individual 909 | // shapes, then the property should go on the collision mesh entity 910 | // 911 | // **Use with the [Manipulator](#manipulator) component** 912 | // 913 | // If a dynamic entity is grabbed by the [Manipulator](#manipulator) component, 914 | // it will temporarily become a kinematic object. This means that collisions 915 | // will no longer impede its movement, and it will track the manipulator 916 | // exactly, (subject to any manipulator constraints, such as 917 | // [`manipulator-weight`](#manipulator-weight)). If you would rather have the 918 | // object remain dynamic, you will need to [redirect the grab](#redirect-grab) 919 | // to a `physx-joint` instead, or even easier, use the 920 | // [`dual-wieldable`](#dual-wieldable) component. 921 | // 922 | // As soon as the dynamic object is released, it will revert back to a dynamic 923 | // object. Objects with the type `kinematic` will remain kinematic. 924 | // 925 | // Static objects should not be moved. If a static object can be the target of a 926 | // manipulator grab (or any other kind of movement), it should be `kinematic` 927 | // instead. 928 | AFRAME.registerComponent('physx-body', { 929 | dependencies: ['physx-material'], 930 | schema: { 931 | // **[dynamic, static, kinematic]** Type of the rigid body to create 932 | type: {default: 'dynamic', oneOf: ['dynamic', 'static', 'kinematic']}, 933 | 934 | // Total mass of the body 935 | mass: {default: 1.0}, 936 | 937 | // If > 0, will set the rigid body's angular damping 938 | angularDamping: {default: 0.0}, 939 | 940 | // If > 0, will set the rigid body's linear damping 941 | linearDamping: {default: 0.0}, 942 | 943 | // If set to `true`, it will emit `contactbegin` and `contactend` events 944 | // when collisions occur 945 | emitCollisionEvents: {default: false}, 946 | 947 | // If set to `true`, the object will receive extra attention by the 948 | // simulation engine (at a performance cost). 949 | highPrecision: {default: false}, 950 | 951 | shapeOffset: {type: 'vec3', default: {x: 0, y: 0, z: 0}} 952 | }, 953 | events: { 954 | stateadded: function(e) { 955 | if (e.detail === 'grabbed') { 956 | this.rigidBody.setRigidBodyFlag(PhysX.PxRigidBodyFlag.eKINEMATIC, true) 957 | } 958 | }, 959 | stateremoved: function(e) { 960 | if (e.detail === 'grabbed') { 961 | if (this.floating) { 962 | this.rigidBody.setLinearVelocity({x: 0, y: 0, z: 0}, true) 963 | } 964 | if (this.data.type !== 'kinematic') 965 | { 966 | this.rigidBody.setRigidBodyFlag(PhysX.PxRigidBodyFlag.eKINEMATIC, false) 967 | } 968 | } 969 | }, 970 | 'bbuttonup': function(e) { 971 | this.toggleGravity() 972 | }, 973 | componentchanged: function(e) { 974 | if (e.name === 'physx-material') 975 | { 976 | this.el.emit('object3dset', {}) 977 | } 978 | }, 979 | object3dset: function(e) { 980 | if (this.rigidBody) { 981 | for (let shape of this.shapes) 982 | { 983 | this.rigidBody.detachShape(shape, false) 984 | } 985 | 986 | let attemptToUseDensity = true; 987 | let seenAnyDensity = false; 988 | let densities = new PhysX.VectorPxReal() 989 | let component = this 990 | let type = this.data.type 991 | let body = this.rigidBody 992 | for (let shape of component.createShapes(this.system.physics, this.system.defaultActorFlags)) 993 | { 994 | body.attachShape(shape) 995 | 996 | if (isFinite(shape.density)) 997 | { 998 | seenAnyDensity = true 999 | densities.push_back(shape.density) 1000 | } 1001 | else 1002 | { 1003 | attemptToUseDensity = false 1004 | 1005 | if (seenAnyDensity) 1006 | { 1007 | console.warn("Densities not set for all shapes. Will use total mass instead.", component.el) 1008 | } 1009 | } 1010 | } 1011 | if (type === 'dynamic' || type === 'kinematic') { 1012 | if (attemptToUseDensity && seenAnyDensity) 1013 | { 1014 | console.log("Setting density vector", densities) 1015 | body.updateMassAndInertia(densities) 1016 | } 1017 | else { 1018 | body.setMassAndUpdateInertia(component.data.mass) 1019 | } 1020 | } 1021 | } 1022 | }, 1023 | contactbegin: function(e) { 1024 | // console.log("Collision", e.detail.points) 1025 | } 1026 | }, 1027 | init() { 1028 | this.system = this.el.sceneEl.systems.physx 1029 | this.physxRegisteredPromise = this.system.registerComponentBody(this, {type: this.data.type}) 1030 | this.el.setAttribute('grab-options', 'scalable', false) 1031 | 1032 | this.kinematicMove = this.kinematicMove.bind(this) 1033 | if (this.el.sceneEl.systems['button-caster']) 1034 | { 1035 | this.el.sceneEl.systems['button-caster'].install(['bbutton']) 1036 | } 1037 | 1038 | this.physxRegisteredPromise.then(() => this.update()) 1039 | }, 1040 | update(oldData) { 1041 | if (!this.rigidBody) return; 1042 | 1043 | if (this.data.type === 'dynamic') 1044 | { 1045 | this.rigidBody.setAngularDamping(this.data.angularDamping) 1046 | this.rigidBody.setLinearDamping(this.data.linearDamping) 1047 | this.rigidBody.setRigidBodyFlag(PhysX.PxRigidBodyFlag.eKINEMATIC, false) 1048 | } 1049 | 1050 | if (this.data.highPrecision) 1051 | { 1052 | if (this.data.type === 'dynamic') { 1053 | this.rigidBody.setSolverIterationCounts(4, 2); 1054 | this.rigidBody.setRigidBodyFlag(PhysX.PxRigidBodyFlag.eENABLE_CCD, true) 1055 | } 1056 | else if (this.data.type === 'kinematic') { 1057 | this.rigidBody.setSolverIterationCounts(4, 2); 1058 | this.rigidBody.setRigidBodyFlag(PhysX.PxRigidBodyFlag.eENABLE_SPECULATIVE_CCD, true); 1059 | } 1060 | } 1061 | 1062 | if (!oldData || this.data.mass !== oldData.mass) this.el.emit('object3dset', {}) 1063 | }, 1064 | remove() { 1065 | if (!this.rigidBody) return; 1066 | this.system.removeBody(this) 1067 | }, 1068 | createGeometry(o) { 1069 | if (o.el.hasAttribute('geometry')) 1070 | { 1071 | let geometry = o.el.getAttribute('geometry'); 1072 | switch(geometry.primitive) 1073 | { 1074 | case 'sphere': 1075 | return new PhysX.PxSphereGeometry(geometry.radius * this.el.object3D.scale.x * 0.98) 1076 | case 'box': 1077 | return new PhysX.PxBoxGeometry(geometry.width / 2, geometry.height / 2, geometry.depth / 2) 1078 | default: 1079 | return this.createConvexMeshGeometry(o.el.getObject3D('mesh')); 1080 | } 1081 | } 1082 | }, 1083 | createConvexMeshGeometry(mesh, rootAncestor) { 1084 | let vectors = new PhysX.PxVec3Vector() 1085 | 1086 | let g = mesh.geometry.attributes.position 1087 | if (!g) return; 1088 | if (g.count < 3) return; 1089 | if (g.itemSize != 3) return; 1090 | let t = new THREE.Vector3; 1091 | 1092 | if (rootAncestor) 1093 | { 1094 | let matrix = new THREE.Matrix4(); 1095 | mesh.updateMatrix(); 1096 | matrix.copy(mesh.matrix) 1097 | let ancestor = mesh.parent; 1098 | while(ancestor && ancestor !== rootAncestor) 1099 | { 1100 | ancestor.updateMatrix(); 1101 | matrix.premultiply(ancestor.matrix); 1102 | ancestor = ancestor.parent; 1103 | } 1104 | for (let i = 0; i < g.count; ++i) { 1105 | t.fromBufferAttribute(g, i) 1106 | t.applyMatrix4(matrix); 1107 | vectors.push_back(Object.assign({}, t)); 1108 | } 1109 | } 1110 | else 1111 | { 1112 | for (let i = 0; i < g.count; ++i) { 1113 | t.fromBufferAttribute(g, i) 1114 | vectors.push_back(Object.assign({}, t)); 1115 | } 1116 | } 1117 | 1118 | let worldScale = new THREE.Vector3; 1119 | let worldBasis = (rootAncestor || mesh); 1120 | worldBasis.updateMatrixWorld(); 1121 | worldBasis.getWorldScale(worldScale); 1122 | let convexMesh = this.system.cooking.createConvexMesh(vectors, this.system.physics) 1123 | return new PhysX.PxConvexMeshGeometry(convexMesh, new PhysX.PxMeshScale({x: worldScale.x, y: worldScale.y, z: worldScale.z}, {w: 1, x: 0, y: 0, z: 0}), new PhysX.PxConvexMeshGeometryFlags(PhysX.PxConvexMeshGeometryFlag.eTIGHT_BOUNDS.value)) 1124 | }, 1125 | createShape(physics, geometry, materialData) 1126 | { 1127 | let material = physics.createMaterial(materialData.staticFriction, materialData.dynamicFriction, materialData.restitution); 1128 | let shape = physics.createShape(geometry, material, false, this.system.defaultActorFlags) 1129 | shape.setQueryFilterData(new PhysX.PxFilterData(PhysXUtil.layersToMask(materialData.collisionLayers), PhysXUtil.layersToMask(materialData.collidesWithLayers), materialData.collisionGroup, 0)) 1130 | shape.setSimulationFilterData(new PhysX.PxFilterData(PhysXUtil.layersToMask(materialData.collisionLayers), PhysXUtil.layersToMask(materialData.collidesWithLayers), materialData.collisionGroup, 0)) 1131 | 1132 | if (materialData.contactOffset >= 0.0) 1133 | { 1134 | shape.setContactOffset(materialData.contactOffset) 1135 | } 1136 | if (materialData.restOffset >= 0.0) 1137 | { 1138 | shape.setRestOffset(materialData.restOffset) 1139 | } 1140 | 1141 | shape.density = materialData.density; 1142 | this.system.registerShape(shape, this) 1143 | 1144 | return shape; 1145 | }, 1146 | createShapes(physics) { 1147 | if (this.el.hasAttribute('geometry')) 1148 | { 1149 | let geometry = this.createGeometry(this.el.object3D); 1150 | if (!geometry) return; 1151 | let materialData = this.el.components['physx-material'].data 1152 | this.shapes = [this.createShape(physics, geometry, materialData)]; 1153 | 1154 | return this.shapes; 1155 | } 1156 | 1157 | let shapes = [] 1158 | Util.traverseCondition(this.el.object3D, 1159 | o => { 1160 | if (o.el && o.el.hasAttribute("physx-no-collision")) return false; 1161 | if (o.el && !o.el.object3D.visible && !o.el.hasAttribute("physx-hidden-collision")) return false; 1162 | if (!o.visible && o.el && !o.el.hasAttribute("physx-hidden-collision")) return false; 1163 | if (o.userData && o.userData.vartisteUI) return false; 1164 | return true 1165 | }, 1166 | o => { 1167 | if (o.geometry) { 1168 | let geometry; 1169 | if (false && o.el && o.el.hasAttribute('geometry')) 1170 | { 1171 | geometry = this.createGeometry(o); 1172 | } 1173 | else 1174 | { 1175 | geometry = this.createConvexMeshGeometry(o, this.el.object3D); 1176 | } 1177 | if (!geometry) { 1178 | console.warn("Couldn't create geometry", o) 1179 | return; 1180 | } 1181 | 1182 | let material, materialData; 1183 | if (o.el && o.el.hasAttribute('physx-material')) 1184 | { 1185 | materialData = o.el.getAttribute('physx-material') 1186 | } 1187 | else 1188 | { 1189 | materialData = this.el.components['physx-material'].data 1190 | } 1191 | let shape = this.createShape(physics, geometry, materialData) 1192 | 1193 | // shape.setLocalPose({translation: this.data.shapeOffset, rotation: {w: 1, x: 0, y: 0, z: 0}}) 1194 | 1195 | shapes.push(shape) 1196 | } 1197 | }); 1198 | 1199 | this.shapes = shapes 1200 | 1201 | return shapes 1202 | }, 1203 | // Turns gravity on and off 1204 | toggleGravity() { 1205 | this.rigidBody.setActorFlag(PhysX.PxActorFlag.eDISABLE_GRAVITY, !this.floating) 1206 | this.floating = !this.floating 1207 | }, 1208 | resetBodyPose() { 1209 | this.rigidBody.setGlobalPose(PhysXUtil.object3DPhysXTransform(this.el.object3D), true) 1210 | }, 1211 | kinematicMove() { 1212 | this.rigidBody.setKinematicTarget(PhysXUtil.object3DPhysXTransform(this.el.object3D)) 1213 | }, 1214 | tock(t, dt) { 1215 | if (this.rigidBody && this.data.type === 'kinematic' && !this.setKinematic) 1216 | { 1217 | this.rigidBody.setRigidBodyFlag(PhysX.PxRigidBodyFlag.eKINEMATIC, true) 1218 | this.setKinematic = true 1219 | } 1220 | if (this.rigidBody && (this.data.type === 'kinematic' || this.el.is("grabbed"))) { 1221 | // this.el.object3D.scale.set(1,1,1) 1222 | this.kinematicMove() 1223 | } 1224 | } 1225 | }) 1226 | 1227 | // Creates a driver which exerts force to return the joint to the specified 1228 | // (currently only the initial) position with the given velocity 1229 | // characteristics. 1230 | // 1231 | // This can only be used on an entity with a `physx-joint` component. Currently 1232 | // only supports **D6** joint type. E.g. 1233 | // 1234 | //``` 1235 | // 1236 | // 1239 | // 1240 | // 1241 | //``` 1242 | AFRAME.registerComponent('physx-joint-driver', { 1243 | dependencies: ['physx-joint'], 1244 | multiple: true, 1245 | schema: { 1246 | // Which axes the joint should operate on. Should be some combination of `x`, `y`, `z`, `twist`, `swing` 1247 | axes: {type: 'array', default: []}, 1248 | 1249 | // How stiff the drive should be 1250 | stiffness: {default: 1.0}, 1251 | 1252 | // Damping to apply to the drive 1253 | damping: {default: 1.0}, 1254 | 1255 | // Maximum amount of force used to get to the target position 1256 | forceLimit: {default: 3.4028234663852885981170418348452e+38}, 1257 | 1258 | // If true, will operate directly on body acceleration rather than on force 1259 | useAcceleration: {default: true}, 1260 | 1261 | // Target linear velocity relative to the joint 1262 | linearVelocity: {type: 'vec3', default: {x: 0, y: 0, z: 0}}, 1263 | 1264 | // Targget angular velocity relative to the joint 1265 | angularVelocity: {type: 'vec3', default: {x: 0, y: 0, z: 0}}, 1266 | 1267 | // If true, will automatically lock axes which are not being driven 1268 | lockOtherAxes: {default: false}, 1269 | 1270 | // If true SLERP rotation mode. If false, will use SWING mode. 1271 | slerpRotation: {default: true}, 1272 | }, 1273 | events: { 1274 | 'physx-jointcreated': function(e) { 1275 | this.setJointDriver() 1276 | } 1277 | }, 1278 | init() { 1279 | this.el.setAttribute('phsyx-custom-constraint', "") 1280 | }, 1281 | setJointDriver() { 1282 | if (!this.enumAxes) this.update(); 1283 | if (this.el.components['physx-joint'].data.type !== 'D6') { 1284 | console.warn("Only D6 joint drivers supported at the moment") 1285 | return; 1286 | } 1287 | 1288 | let PhysX = this.el.sceneEl.systems.physx.PhysX; 1289 | this.joint = this.el.components['physx-joint'].joint 1290 | 1291 | if (this.data.lockOtherAxes) 1292 | { 1293 | this.joint.setMotion(PhysX.PxD6Axis.eX, PhysX.PxD6Motion.eLOCKED) 1294 | this.joint.setMotion(PhysX.PxD6Axis.eY, PhysX.PxD6Motion.eLOCKED) 1295 | this.joint.setMotion(PhysX.PxD6Axis.eZ, PhysX.PxD6Motion.eLOCKED) 1296 | this.joint.setMotion(PhysX.PxD6Axis.eSWING1, PhysX.PxD6Motion.eLOCKED) 1297 | this.joint.setMotion(PhysX.PxD6Axis.eSWING2, PhysX.PxD6Motion.eLOCKED) 1298 | this.joint.setMotion(PhysX.PxD6Axis.eTWIST, PhysX.PxD6Motion.eLOCKED) 1299 | } 1300 | 1301 | for (let enumKey of this.enumAxes) 1302 | { 1303 | this.joint.setMotion(enumKey, PhysX.PxD6Motion.eFREE) 1304 | } 1305 | 1306 | let drive = new PhysX.PxD6JointDrive; 1307 | drive.stiffness = this.data.stiffness; 1308 | drive.damping = this.data.damping; 1309 | drive.forceLimit = this.data.forceLimit; 1310 | drive.setAccelerationFlag(this.data.useAcceleration); 1311 | 1312 | for (let axis of this.driveAxes) 1313 | { 1314 | this.joint.setDrive(axis, drive); 1315 | } 1316 | 1317 | console.log("Setting joint driver", this.driveAxes, this.enumAxes) 1318 | 1319 | this.joint.setDrivePosition({translation: {x: 0, y: 0, z: 0}, rotation: {w: 1, x: 0, y: 0, z: 0}}, true) 1320 | 1321 | this.joint.setDriveVelocity(this.data.linearVelocity, this.data.angularVelocity, true); 1322 | }, 1323 | update(oldData) { 1324 | if (!PhysX) return; 1325 | 1326 | this.enumAxes = [] 1327 | for (let axis of this.data.axes) 1328 | { 1329 | if (axis === 'swing') { 1330 | this.enumAxes.push(PhysX.PxD6Axis.eSWING1) 1331 | this.enumAxes.push(PhysX.PxD6Axis.eSWING2) 1332 | continue 1333 | } 1334 | let enumKey = `e${axis.toUpperCase()}` 1335 | if (!(enumKey in PhysX.PxD6Axis)) 1336 | { 1337 | console.warn(`Unknown axis ${axis} (PxD6Axis::${enumKey})`) 1338 | } 1339 | this.enumAxes.push(PhysX.PxD6Axis[enumKey]) 1340 | } 1341 | 1342 | this.driveAxes = [] 1343 | 1344 | for (let axis of this.data.axes) 1345 | { 1346 | if (axis === 'swing') { 1347 | if (this.data.slerpRotation) 1348 | { 1349 | this.driveAxes.push(PhysX.PxD6Drive.eSLERP) 1350 | } 1351 | else 1352 | { 1353 | this.driveAxes.push(PhysX.PxD6Drive.eSWING) 1354 | } 1355 | continue 1356 | } 1357 | 1358 | if (axis === 'twist' && this.data.slerpRotation) { 1359 | this.driveAxes.push(PhysX.PxD6Drive.eSLERP) 1360 | continue; 1361 | } 1362 | 1363 | let enumKey = `e${axis.toUpperCase()}` 1364 | if (!(enumKey in PhysX.PxD6Drive)) 1365 | { 1366 | console.warn(`Unknown axis ${axis} (PxD6Axis::${enumKey})`) 1367 | } 1368 | this.driveAxes.push(PhysX.PxD6Drive[enumKey]) 1369 | } 1370 | } 1371 | }) 1372 | 1373 | // See README.md for examples 1374 | AFRAME.registerComponent('physx-joint-constraint', { 1375 | multiple: true, 1376 | schema: { 1377 | // Which axes are explicitly locked by this constraint and can't be moved at all. 1378 | // Should be some combination of `x`, `y`, `z`, `twist`, `swing` 1379 | lockedAxes: {type: 'array', default: []}, // for D6 joint type 1380 | 1381 | // Which axes are constrained by this constraint. These axes can be moved within the set limits. 1382 | // Should be some combination of `x`, `y`, `z`, `twist`, `swing` 1383 | constrainedAxes: {type: 'array', default: []}, // for D6 joint type 1384 | 1385 | // Which axes are explicitly freed by this constraint. These axes will not obey any limits set here. 1386 | // Should be some combination of `x`, `y`, `z`, `twist`, `swing` 1387 | freeAxes: {type: 'array', default: []}, // for D6 joint type 1388 | 1389 | // Limit on linear movement. Only affects `x`, `y`, and `z` axes. 1390 | // First vector component is the minimum allowed position 1391 | linearLimit: {type: 'vec2'}, // for D6 and Prismatic joint type 1392 | 1393 | // Limit on angular movement in degrees. Example: `-110 80` to move between -110 and 80 degrees 1394 | angularLimit: {type: 'vec2'}, // for Revolute joint type 1395 | 1396 | // Two angles in degrees specifying a cone in which the joint is allowed to swing, like 1397 | // a pendulum. 1398 | limitCone: {type: 'vec2'}, // for D6 joint type 1399 | 1400 | // Minimum and maximum angles in degrees that the joint is allowed to twist 1401 | twistLimit: {type: 'vec2'}, // for D6 joint type 1402 | 1403 | // Spring damping for soft constraints 1404 | damping: {default: 0.0}, 1405 | // Spring restitution for soft constraints 1406 | restitution: {default: 0.0}, 1407 | // If greater than 0, will make this joint a soft constraint, and use a spring force model 1408 | stiffness: {default: 0.0}, 1409 | }, 1410 | events: { 1411 | 'physx-jointcreated': function(e) { 1412 | this.setJointConstraint() 1413 | } 1414 | }, 1415 | init() { 1416 | this.propsInitialized = false; 1417 | this.el.setAttribute('phsyx-custom-constraint', "") 1418 | }, 1419 | setJointConstraint() { 1420 | const jointType = this.el.components['physx-joint'].data.type; 1421 | if (jointType !== 'D6' && jointType !== 'Revolute' && jointType !== 'Prismatic') { 1422 | console.warn("Only D6, Revolute and Prismatic joint constraints supported at the moment") 1423 | return; 1424 | } 1425 | 1426 | if (!this.propsInitialized) this.update(); 1427 | 1428 | const joint = this.el.components['physx-joint'].joint; 1429 | 1430 | if (jointType === 'Revolute') { 1431 | // https://nvidiagameworks.github.io/PhysX/4.1/documentation/physxapi/files/classPxJointAngularLimitPair.html 1432 | const spring = new PhysX.PxSpring(this.data.stiffness, this.data.damping); 1433 | const limitPair = new PhysX.PxJointAngularLimitPair( 1434 | -THREE.MathUtils.degToRad(this.data.angularLimit.y), 1435 | -THREE.MathUtils.degToRad(this.data.angularLimit.x), 1436 | spring) 1437 | limitPair.restitution = this.data.restitution; 1438 | joint.setLimit(limitPair); 1439 | joint.setRevoluteJointFlag(PhysX.PxRevoluteJointFlag.eLIMIT_ENABLED, true); 1440 | } 1441 | 1442 | if (jointType === 'Prismatic') { 1443 | const spring = new PhysX.PxSpring(this.data.stiffness, this.data.damping); 1444 | const limitPair = new PhysX.PxJointLinearLimitPair(-this.data.linearLimit.y, -this.data.linearLimit.x, spring); 1445 | limitPair.restitution = this.data.restitution; 1446 | joint.setLimit(limitPair); 1447 | joint.setPrismaticJointFlag(PhysX.PxPrismaticJointFlag.eLIMIT_ENABLED, true); 1448 | } 1449 | 1450 | if (jointType === 'D6') { 1451 | const createLinearLimit = (axis) => { 1452 | const spring = new PhysX.PxSpring(this.data.stiffness, this.data.damping); 1453 | let limitPair; 1454 | if (axis === PhysX.PxD6Axis.eX || axis === PhysX.PxD6Axis.eZ) { 1455 | limitPair = new PhysX.PxJointLinearLimitPair(-this.data.linearLimit.y, -this.data.linearLimit.x, spring); 1456 | } else { 1457 | limitPair = new PhysX.PxJointLinearLimitPair(this.data.linearLimit.x, this.data.linearLimit.y, spring); 1458 | } 1459 | limitPair.restitution = this.data.restitution; 1460 | return limitPair; 1461 | } 1462 | 1463 | for (let axis of this.freeAxes) 1464 | { 1465 | joint.setMotion(axis, PhysX.PxD6Motion.eFREE) 1466 | } 1467 | 1468 | for (let axis of this.lockedAxes) 1469 | { 1470 | joint.setMotion(axis, PhysX.PxD6Motion.eLOCKED) 1471 | } 1472 | 1473 | for (let axis of this.constrainedAxes) 1474 | { 1475 | if (axis === PhysX.PxD6Axis.eX || axis === PhysX.PxD6Axis.eY || axis === PhysX.PxD6Axis.eZ) 1476 | { 1477 | joint.setMotion(axis, PhysX.PxD6Motion.eLIMITED) 1478 | joint.setLinearLimit(axis, createLinearLimit(axis)) 1479 | continue; 1480 | } 1481 | 1482 | if (axis === PhysX.PxD6Axis.eTWIST) 1483 | { 1484 | joint.setMotion(PhysX.PxD6Axis.eTWIST, PhysX.PxD6Motion.eLIMITED) 1485 | const spring = new PhysX.PxSpring(this.data.stiffness, this.data.damping); 1486 | const limitPair = new PhysX.PxJointAngularLimitPair( 1487 | -THREE.MathUtils.degToRad(this.data.twistLimit.y), 1488 | -THREE.MathUtils.degToRad(this.data.twistLimit.x), 1489 | spring) 1490 | limitPair.restitution = this.data.restitution; 1491 | joint.setTwistLimit(limitPair) 1492 | continue; 1493 | } 1494 | 1495 | joint.setMotion(axis, PhysX.PxD6Motion.eLIMITED) 1496 | const spring = new PhysX.PxSpring(this.data.stiffness, this.data.damping); 1497 | const cone = new PhysX.PxJointLimitCone( 1498 | -THREE.MathUtils.degToRad(this.data.limitCone.y), 1499 | -THREE.MathUtils.degToRad(this.data.limitCone.x), 1500 | spring) 1501 | cone.restitution = this.data.restitution; 1502 | joint.setSwingLimit(cone) 1503 | } 1504 | } 1505 | }, 1506 | update(oldData) { 1507 | if (!PhysX) return; 1508 | 1509 | this.propsInitialized = true; 1510 | this.constrainedAxes = PhysXUtil.axisArrayToEnums(this.data.constrainedAxes) 1511 | this.lockedAxes = PhysXUtil.axisArrayToEnums(this.data.lockedAxes) 1512 | this.freeAxes = PhysXUtil.axisArrayToEnums(this.data.freeAxes) 1513 | } 1514 | }) 1515 | 1516 | // Creates a PhysX joint between an ancestor rigid body and a target rigid body. 1517 | // 1518 | // The physx-joint is designed to be used either on or within an entity with the 1519 | // `physx-body` component. For instance: 1520 | // 1521 | // ``` 1522 | // 1523 | // 1524 | // 1525 | // ``` 1526 | // 1527 | // The position and rotation of the `physx-joint` will be used to create the 1528 | // corresponding PhysX joint object. Multiple joints can be created on a body, 1529 | // and multiple joints can target a body. 1530 | // 1531 | // **Stapler Example** 1532 | // 1533 | // Here's a simplified version of the stapler from the [physics playground demo]() 1534 | // 1535 | //``` 1536 | // 1537 | // 1538 | // 1539 | // 1540 | // 1541 | // 1542 | // 1543 | //``` 1544 | // 1545 | // Notice the joint is created between the top part of the stapler (which 1546 | // contains the joint) and the bottom part of the stapler at the position of the 1547 | // `physx-joint` component's entitiy. This will be the pivot point for the 1548 | // stapler's rotation. 1549 | // 1550 | // ![Stapler with joint highlighted](./static/images/staplerjoint.png) 1551 | AFRAME.registerComponent('physx-joint', { 1552 | multiple: true, 1553 | schema: { 1554 | // Rigid body joint type to use. See the [NVIDIA PhysX joint 1555 | // documentation](https://nvidiagameworks.github.io/PhysX/4.1/documentation/physxguide/Manual/Joints.html) 1556 | // for details on each type 1557 | type: {default: "Spherical", oneOf: ["Fixed", "Spherical", "Distance", "Revolute", "Prismatic", "D6"]}, 1558 | 1559 | // Target object. If specified, must be an entity having the `physx-body` 1560 | // component. If no target is specified, a scene default target will be 1561 | // used, essentially joining the joint to its initial position in the world. 1562 | target: {type: 'selector'}, 1563 | 1564 | // Force needed to break the constraint. First component is the linear force, second component is angular force in degrees. Set both components are >= 0 1565 | breakForce: {type: 'vec2', default: {x: -1, y: -1}}, 1566 | 1567 | // If true, removes the entity containing this component when the joint is 1568 | // broken. 1569 | removeElOnBreak: {default: false}, 1570 | 1571 | // If false, collision will be disabled between the rigid body containing 1572 | // the joint and the target rigid body. 1573 | collideWithTarget: {default: false}, 1574 | 1575 | // When used with a D6 type, sets up a "soft" fixed joint. E.g., for grabbing things 1576 | softFixed: {default: false}, 1577 | 1578 | // Kinematic projection, which forces joint back into alignment when the solver fails. 1579 | // First component is the linear tolerance in meters, second component is angular tolerance in degrees. Set both components are >= 0 1580 | // See: https://nvidiagameworks.github.io/PhysX/4.1/documentation/physxguide/Manual/Joints.html#projection 1581 | projectionTolerance: {type: 'vec2', default: {x: -1, y: -1}}, 1582 | }, 1583 | events: { 1584 | constraintbreak: function(e) { 1585 | if (this.data.removeElOnBreak) { 1586 | this.el.remove() 1587 | } 1588 | } 1589 | }, 1590 | init() { 1591 | this.system = this.el.sceneEl.systems.physx 1592 | 1593 | let parentEl = this.el 1594 | 1595 | while (parentEl && !parentEl.hasAttribute('physx-body')) 1596 | { 1597 | parentEl = parentEl.parentEl 1598 | } 1599 | 1600 | if (!parentEl) { 1601 | console.warn("physx-joint must be used within a physx-body") 1602 | return; 1603 | } 1604 | 1605 | this.bodyEl = parentEl 1606 | 1607 | this.worldHelper = new THREE.Object3D; 1608 | this.worldHelperParent = new THREE.Object3D; 1609 | this.el.sceneEl.object3D.add(this.worldHelperParent); 1610 | this.targetScale = new THREE.Vector3(1,1,1) 1611 | this.worldHelperParent.add(this.worldHelper) 1612 | 1613 | if (!this.data.target) { 1614 | this.data.target = this.system.defaultTarget 1615 | } 1616 | 1617 | 1618 | Util.whenLoaded([this.el, this.bodyEl, this.data.target], () => { 1619 | this.createJoint() 1620 | }) 1621 | }, 1622 | remove() { 1623 | if (this.joint) { 1624 | this.joint.release(); 1625 | this.joint = null; 1626 | this.bodyEl.components['physx-body'].rigidBody.wakeUp() 1627 | if (this.data.target.components['physx-body'].rigidBody.wakeUp) this.data.target.components['physx-body'].rigidBody.wakeUp() 1628 | } 1629 | }, 1630 | update() { 1631 | if (!this.joint) return; 1632 | 1633 | if (this.data.breakForce.x >= 0 && this.data.breakForce.y >= 0) 1634 | { 1635 | this.joint.setBreakForce(this.data.breakForce.x, THREE.MathUtils.degToRad(this.data.breakForce.y)); 1636 | } 1637 | 1638 | this.joint.setConstraintFlag(PhysX.PxConstraintFlag.eCOLLISION_ENABLED, this.data.collideWithTarget) 1639 | 1640 | if (this.data.projectionTolerance.x >= 0 && this.data.projectionTolerance.y >= 0) { 1641 | this.joint.setProjectionLinearTolerance(this.data.projectionTolerance.x) 1642 | this.joint.setProjectionAngularTolerance(THREE.MathUtils.degToRad(this.data.projectionTolerance.y)) 1643 | this.joint.setConstraintFlag(PhysX.PxConstraintFlag.ePROJECTION, true); 1644 | } 1645 | else 1646 | { 1647 | this.joint.setConstraintFlag(PhysX.PxConstraintFlag.ePROJECTION, false); 1648 | } 1649 | 1650 | if (this.el.hasAttribute('phsyx-custom-constraint')) return; 1651 | 1652 | switch (this.data.type) 1653 | { 1654 | case 'D6': 1655 | { 1656 | if (this.data.softFixed) 1657 | { 1658 | this.joint.setMotion(PhysX.PxD6Axis.eX, PhysX.PxD6Motion.eFREE) 1659 | this.joint.setMotion(PhysX.PxD6Axis.eY, PhysX.PxD6Motion.eFREE) 1660 | this.joint.setMotion(PhysX.PxD6Axis.eZ, PhysX.PxD6Motion.eFREE) 1661 | this.joint.setMotion(PhysX.PxD6Axis.eSWING1, PhysX.PxD6Motion.eFREE) 1662 | this.joint.setMotion(PhysX.PxD6Axis.eSWING2, PhysX.PxD6Motion.eFREE) 1663 | this.joint.setMotion(PhysX.PxD6Axis.eTWIST, PhysX.PxD6Motion.eFREE) 1664 | 1665 | let drive = new PhysX.PxD6JointDrive; 1666 | drive.stiffness = 1000; 1667 | drive.damping = 500; 1668 | drive.forceLimit = 1000; 1669 | drive.setAccelerationFlag(false); 1670 | this.joint.setDrive(PhysX.PxD6Drive.eX, drive); 1671 | this.joint.setDrive(PhysX.PxD6Drive.eY, drive); 1672 | this.joint.setDrive(PhysX.PxD6Drive.eZ, drive); 1673 | // this.joint.setDrive(PhysX.PxD6Drive.eSWING, drive); 1674 | // this.joint.setDrive(PhysX.PxD6Drive.eTWIST, drive); 1675 | this.joint.setDrive(PhysX.PxD6Drive.eSLERP, drive); 1676 | this.joint.setDrivePosition({translation: {x: 0, y: 0, z: 0}, rotation: {w: 1, x: 0, y: 0, z: 0}}, true) 1677 | this.joint.setDriveVelocity({x: 0.0, y: 0.0, z: 0.0}, {x: 0, y: 0, z: 0}, true); 1678 | } 1679 | } 1680 | break; 1681 | } 1682 | }, 1683 | getTransform(el) { 1684 | Util.positionObject3DAtTarget(this.worldHelperParent, el.object3D, {scale: this.targetScale}) 1685 | 1686 | Util.positionObject3DAtTarget(this.worldHelper, this.el.object3D, {scale: this.targetScale}); 1687 | 1688 | let transform = PhysXUtil.matrixToTransform(this.worldHelper.matrix); 1689 | 1690 | return transform; 1691 | }, 1692 | async createJoint() { 1693 | await Util.whenComponentInitialized(this.bodyEl, 'physx-body') 1694 | await Util.whenComponentInitialized(this.data.target, 'physx-body') 1695 | await this.bodyEl.components['physx-body'].physxRegisteredPromise; 1696 | await this.data.target.components['physx-body'].physxRegisteredPromise; 1697 | 1698 | if (this.joint) { 1699 | this.joint.release(); 1700 | this.joint = null; 1701 | } 1702 | 1703 | let thisTransform = this.getTransform(this.bodyEl); 1704 | let targetTransform = this.getTransform(this.data.target); 1705 | 1706 | this.joint = PhysX[`Px${this.data.type}JointCreate`](this.system.physics, 1707 | this.bodyEl.components['physx-body'].rigidBody, thisTransform, 1708 | this.data.target.components['physx-body'].rigidBody, targetTransform, 1709 | ) 1710 | this.system.registerJoint(this.joint, this) 1711 | this.update(); 1712 | this.el.emit('physx-jointcreated', this.joint) 1713 | } 1714 | }) 1715 | 1716 | 1717 | AFRAME.registerSystem('physx-contact-event', { 1718 | init() { 1719 | this.worldHelper = new THREE.Object3D; 1720 | this.el.sceneEl.object3D.add(this.worldHelper) 1721 | } 1722 | }) 1723 | 1724 | // Emits a `contactevent` event when a collision meets the threshold. This 1725 | // should be set on an entity with the `physx-body` component. The event detail 1726 | // will contain these fields: 1727 | // - `impulse`: The summed impulse of at all contact points 1728 | // - `contact`: The originating contact event 1729 | AFRAME.registerComponent('physx-contact-event', { 1730 | dependencies: ['physx-body'], 1731 | schema: { 1732 | // Minimum total impulse threshold to emit the event 1733 | impulseThreshold: {default: 0.01}, 1734 | 1735 | // NYI 1736 | maxDistance: {default: 10.0}, 1737 | // NYI 1738 | maxDuration: {default: 5.0}, 1739 | 1740 | // Delay after start of scene before emitting events. Useful to avoid a 1741 | // zillion events as objects initially settle on the ground 1742 | startDelay: {default: 6000}, 1743 | 1744 | // If `true`, the event detail will include a `positionWorld` property which contains the weighted averaged location 1745 | // of all contact points. Contact points are weighted by impulse amplitude. 1746 | positionAtContact: {default: false}, 1747 | }, 1748 | events: { 1749 | contactbegin: function(e) { 1750 | if (this.el.sceneEl.time < this.data.startDelay) return 1751 | let thisWorld = this.eventDetail.positionWorld; 1752 | let cameraWorld = this.pool('cameraWorld', THREE.Vector3); 1753 | 1754 | let impulses = e.detail.impulses 1755 | let impulseSum = 0 1756 | for (let i = 0; i < impulses.size(); ++i) 1757 | { 1758 | impulseSum += impulses.get(i) 1759 | } 1760 | 1761 | if (impulseSum < this.data.impulseThreshold) return; 1762 | 1763 | thisWorld.set(0, 0, 0) 1764 | let impulse = 0.0; 1765 | if (this.data.positionAtContact) 1766 | { 1767 | for (let i = 0; i < impulses.size(); ++i) 1768 | { 1769 | impulse = impulses.get(i); 1770 | let position = e.detail.points.get(i); 1771 | thisWorld.x += position.x * impulse; 1772 | thisWorld.y += position.y * impulse; 1773 | thisWorld.z += position.z * impulse; 1774 | } 1775 | thisWorld.multiplyScalar(1.0 / impulseSum) 1776 | this.system.worldHelper.position.copy(thisWorld) 1777 | Util.positionObject3DAtTarget(this.localHelper, this.system.worldHelper) 1778 | this.eventDetail.position.copy(this.localHelper.position) 1779 | } 1780 | else 1781 | { 1782 | thisWorld.set(0, 0, 0) 1783 | this.eventDetail.position.set(0, 0, 0) 1784 | } 1785 | 1786 | this.eventDetail.impulse = impulseSum 1787 | this.eventDetail.contact = e.detail 1788 | 1789 | this.el.emit('contactevent', this.eventDetail) 1790 | } 1791 | }, 1792 | init() { 1793 | VARTISTE.Pool.init(this) 1794 | 1795 | this.eventDetail = { 1796 | impulse: 0.0, 1797 | positionWorld: new THREE.Vector3(), 1798 | position: new THREE.Vector3(), 1799 | contact: null, 1800 | } 1801 | 1802 | if (this.data.debug) { 1803 | let vis = document.createElement('a-entity') 1804 | vis.setAttribute('geometry', 'primitive: sphere; radius: 0.1') 1805 | vis.setAttribute('physx-no-collision', '') 1806 | } 1807 | 1808 | this.localHelper = new THREE.Object3D(); 1809 | this.el.object3D.add(this.localHelper) 1810 | 1811 | this.el.setAttribute('physx-body', 'emitCollisionEvents', true) 1812 | }, 1813 | remove() { 1814 | this.el.object3D.remove(this.localHelper) 1815 | } 1816 | }) 1817 | 1818 | // Plays a sound when a `physx-body` has a collision. 1819 | AFRAME.registerComponent('physx-contact-sound', { 1820 | dependencies: ['physx-contact-event'], 1821 | schema: { 1822 | // Sound file location or asset 1823 | src: {type: 'string'}, 1824 | 1825 | // Minimum total impulse to play the sound 1826 | impulseThreshold: {default: 0.01}, 1827 | 1828 | // NYI 1829 | maxDistance: {default: 10.0}, 1830 | // NYI 1831 | maxDuration: {default: 5.0}, 1832 | 1833 | // Delay after start of scene before playing sounds. Useful to avoid a 1834 | // zillion sounds playing as objects initially settle on the ground 1835 | startDelay: {default: 6000}, 1836 | 1837 | // If `true`, the sound will be positioned at the weighted averaged location 1838 | // of all contact points. Contact points are weighted by impulse amplitude. 1839 | // If `false`, the sound will be positioned at the entity's origin. 1840 | positionAtContact: {default: false}, 1841 | }, 1842 | events: { 1843 | contactevent: function(e) { 1844 | if (this.data.positionAtContact) 1845 | { 1846 | this.sound.object3D.position.copy(e.detail.position) 1847 | } 1848 | 1849 | this.sound.components.sound.stopSound(); 1850 | this.sound.components.sound.playSound(); 1851 | }, 1852 | }, 1853 | init() { 1854 | let sound = document.createElement('a-entity') 1855 | this.el.append(sound) 1856 | sound.setAttribute('sound', {src: this.data.src}) 1857 | this.sound = sound 1858 | 1859 | this.el.setAttribute('physx-body', 'emitCollisionEvents', true) 1860 | }, 1861 | update(oldData) { 1862 | this.el.setAttribute('physx-contact-event', this.data) 1863 | } 1864 | }) 1865 | 1866 | // Creates A-Frame entities from gltf custom properties. 1867 | // 1868 | // **WARNING** do not use this component with untrusted gltf models, since it 1869 | // will let the model access arbitrary components. 1870 | // 1871 | // Should be set on an entity with the `gltf-model` component. Once the model is 1872 | // loaded, this will traverse the object tree, and any objects containing user 1873 | // data key `a-entity` will be turned into separate sub-entities. The user data 1874 | // value for `a-entity` will be set as the attributes. 1875 | // 1876 | // For instance, say you export a model with the following kind of structure 1877 | // from Blender (remembering to check "Include → Custom Properties"!): 1878 | // 1879 | //``` 1880 | // - Empty1 1881 | // Custom Properties: 1882 | // name: a-entity 1883 | // value: physx-body="type: dynamic" 1884 | // Children: 1885 | // - Mesh1 1886 | // Custom Properties: 1887 | // name: a-entity 1888 | // value: physx-material="density: 30" class="clickable" 1889 | // Children: 1890 | // - Mesh2 1891 | // - Mesh3 1892 | // Custom Properties: 1893 | // name: a-entity 1894 | // value: physx-material="density: 100" physx-contact-sound="src: #boom" 1895 | //``` 1896 | // 1897 | // ![Screenshot showing the structure in Blender](./static/images/blenderentities.png) 1898 | // 1899 | // This will turn into the following HTML (with `setId` set to `true`): 1900 | // 1901 | //``` 1902 | // 1903 | // 1904 | // 1905 | // 1906 | //``` 1907 | // 1908 | // **Experimental Blender Plugin** 1909 | // 1910 | // ![Screenshot showing experimental blender plugin](./static/images/blenderplugin.png) 1911 | // 1912 | // I've written a small plugin for [Blender](https://www.blender.org/) which can 1913 | // automatically set up a lot of the common properties for use in this physics 1914 | // system. _Note that it is super experimental and under development. Make a 1915 | // backup before using._ 1916 | // 1917 | // **Download Blender Plugin:** vartiste_toolkit_entity_helper.zip (v0.2.0) 1918 | // 1919 | // **GLB Viewer** 1920 | // 1921 | // You can test your `gltf-entities` enabled glb files locally by dragging and 1922 | // dropping them into this [web viewer](https://fascinated-hip-period.glitch.me/viewer.html) 1923 | AFRAME.registerComponent('gltf-entities', { 1924 | dependencies: ['gltf-model'], 1925 | schema: { 1926 | // If true, will set created element's id based on the gltf object name 1927 | setId: {default: false}, 1928 | // If `setId` is true, this will be prepended to the gltf object name when setting the element id 1929 | idPrefix: {default: ""}, 1930 | 1931 | // Automatically make entities clickable and propogate the grab (for use with [`manipulator`](#manipulator)) 1932 | autoPropogateGrab: {default: true}, 1933 | 1934 | // Array of attribute names that should be copied from this entitiy to any new created entitity 1935 | copyAttributes: {type: 'array'}, 1936 | 1937 | // A list of names of attributes that are allowed to be set. Ignored if empty. 1938 | allowedAttributes: {type: 'array'}, 1939 | }, 1940 | events: { 1941 | 'model-loaded': function(e) { 1942 | this.setupEntities() 1943 | } 1944 | }, 1945 | init() {}, 1946 | setupEntities() { 1947 | let root = this.el.getObject3D('mesh') 1948 | if (!root) return; 1949 | 1950 | this.setupObject(root, this.el) 1951 | }, 1952 | setupObject(obj3d, currentRootEl) 1953 | { 1954 | if (obj3d.userData['a-entity']) { 1955 | let el = document.createElement('a-entity') 1956 | let attrs = obj3d.userData['a-entity'] 1957 | 1958 | // sanitize 1959 | el.innerHTML = attrs 1960 | el.innerHTML = `` 1961 | 1962 | el = el.children[0] 1963 | 1964 | if (this.data.allowedAttributes.length) 1965 | { 1966 | for (let attr of el.attributes) 1967 | { 1968 | if (!this.data.allowedAttributes.includes(attr.name)) 1969 | { 1970 | el.removeAttribute(attr.name) 1971 | } 1972 | } 1973 | } 1974 | 1975 | if (this.data.setId && obj3d.name) 1976 | { 1977 | el.id = `${this.data.idPrefix}${obj3d.name}` 1978 | } 1979 | 1980 | for (let attribute of this.data.copyAttributes) 1981 | { 1982 | if (this.el.hasAttribute(attribute)) 1983 | { 1984 | el.setAttribute(attribute, this.el.getAttribute(attribute)) 1985 | } 1986 | } 1987 | 1988 | if (this.data.autoPropogateGrab && this.el.classList.contains("clickable")) 1989 | { 1990 | el.setAttribute('propogate-grab', "") 1991 | el.classList.add("clickable") 1992 | } 1993 | 1994 | currentRootEl.append(el) 1995 | Util.whenLoaded(el, () => { 1996 | el.setObject3D('mesh', obj3d) 1997 | obj3d.updateMatrix() 1998 | Util.applyMatrix(obj3d.matrix, el.object3D) 1999 | obj3d.matrix.identity() 2000 | Util.applyMatrix(obj3d.matrix, obj3d) 2001 | }) 2002 | currentRootEl = el 2003 | } 2004 | 2005 | for (let child of obj3d.children) 2006 | { 2007 | this.setupObject(child, currentRootEl) 2008 | } 2009 | } 2010 | }) 2011 | -------------------------------------------------------------------------------- /styles.css: -------------------------------------------------------------------------------- 1 | 2 | .code-link { 3 | position: fixed; 4 | right: 50px; 5 | top: 50px; 6 | width: 70px; 7 | height: 70px; 8 | border-radius: 50%; 9 | display: flex; 10 | background: white; 11 | justify-content: center; 12 | align-items: center; 13 | font-size: 20px; 14 | text-decoration: none; 15 | text-align: center; 16 | color: black; 17 | font-family: courier; 18 | font-weight: bold; 19 | z-index: 10; 20 | } 21 | 22 | .text-overlay { 23 | background: black; 24 | color: white; 25 | width: 40%; 26 | position: absolute; 27 | bottom: 10px; 28 | left: 30%; 29 | z-index: 10; 30 | font-size: 20px; 31 | font-family: arial; 32 | display: flex; 33 | flex-direction: column; 34 | align-items: left; 35 | } 36 | 37 | .text-overlay p { 38 | margin: 0.2em 1em; 39 | } 40 | -------------------------------------------------------------------------------- /wasm/physx.release.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c-frame/physx/383ff2c335ff48216d02092c60d35fc3386019ff/wasm/physx.release.wasm -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | // Webpack uses this to work with directories 2 | const path = require('path'); 3 | 4 | // This is the main configuration object. 5 | // Here, you write different options and tell Webpack what to do 6 | module.exports = { 7 | 8 | // Path to your entry point. From this file Webpack will begin its work 9 | entry: './index.js', 10 | 11 | // Path and filename of your result bundle. 12 | // Webpack will bundle all JavaScript into this file 13 | output: { 14 | path: path.resolve(__dirname, 'dist'), 15 | publicPath: '/dist/', 16 | filename: 'physx.js' 17 | }, 18 | 19 | resolve: { 20 | fallback: { 21 | "fs": false, 22 | "path": false 23 | }, 24 | }, 25 | 26 | devServer: { 27 | server: { 28 | type: "https" 29 | }, 30 | 31 | static: { 32 | directory: path.join(__dirname, ''), 33 | }, 34 | 35 | onListening: function (devServer) { 36 | if (!devServer) { 37 | throw new Error('webpack-dev-server is not defined'); 38 | } 39 | const brightYellow = "\x1b[1m\x1b[33m" 40 | const underscoreCyan = "\x1b[4m\x1b[36m" 41 | const reset = "\x1b[0m" 42 | 43 | const port = devServer.server.address().port; 44 | console.log(brightYellow) 45 | console.log("***********************************************************************"); 46 | console.log(`* View examples at ${underscoreCyan}http://localhost:${port}/examples${reset}${brightYellow} *`); 47 | console.log("***********************************************************************"); 48 | console.log(reset) 49 | }, 50 | }, 51 | 52 | // Default mode for Webpack is production. 53 | // Depending on mode Webpack will apply different things 54 | // on the final bundle. For now, we don't need production's JavaScript 55 | // minifying and other things, so let's set mode to development 56 | mode: 'development' 57 | }; -------------------------------------------------------------------------------- /webpack.prod.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var merge = require('webpack-merge').merge; 3 | var commonConfiguration = require('./webpack.config.js'); 4 | 5 | module.exports = merge(commonConfiguration, { 6 | output: { 7 | path: path.resolve(__dirname, 'dist'), 8 | publicPath: '/dist/', 9 | filename: 'physx.min.js' 10 | }, 11 | mode: 'production' 12 | }); --------------------------------------------------------------------------------