Lets say I have a cylinder model that I load into my webvr scene. How can I make the object solid? I.e. the user (camera view) cannot take any position within the object or
This depends on what devices you plan to support, and how you allow users to navigate your scene. For most VR experiences, follow best practices and only move the camera proportionately to the user's motion. If the user steps forward in roomscale space and the camera is "blocked," this is a very bad experience. For most VR applications it's better to do locomotion with teleportation, design your scene to keep obstacles out of the way, or explore more creative ways of moving users through the world.
For non-VR desktop experiences with a gamepad or WASD controls, or for VR scenes where the camera is inside a vehicle, you can add a physics engine to prevent movement through obstacles.
^I'll try to get this added to the A-Frame FAQ or documentation soon. This has been added to the A-Frame FAQ. Here's an example using checkpoints, and an example using a physics engine.
Since I landed here looking for an easier answer (not dealing with custom, but standard AFrame objects), maybe this is not what you want. But if you are just asking for "preventing the camera going through AFrame objects", maybe this will help you.
In the example below, I use AFrame Physics System to manage the physics.
I've copied the kinematic-body
component from AFrame Extras, with the name kinema-body
(see embedded script) just because it is labeled as "deprecated", so it may dissapear in the future.
You'll see how you can move in the scene, but when reaching an object, you cannot move through it. This works for me both in desktop and in Oculus Go. The trick is easy: the camera is embodied in a kinema-body
rig, and objects (plane and box) are static-body
. When kinema-body
touches static-body
, it stops (well, or tries to move around it, depending on how it was moving).
<!DOCTYPE html>
<html>
<head>
<script src="//aframe.io/releases/0.8.2/aframe.min.js"></script>
<script src="//cdn.rawgit.com/donmccurdy/aframe-extras/v5.0.0/dist/aframe-extras.min.js"></script>
<script src="//cdn.rawgit.com/donmccurdy/aframe-physics-system/v3.3.0/dist/aframe-physics-system.min.js"></script>
<script>
/**
* Kinema body.
*
* Based on kinematic-body, from AFrame Extras (for now, basically a copy,
* just because I read it is deprecated in AFrame Extras)
*
* https://github.com/donmccurdy/aframe-extras/blob/master/src/misc/kinematic-body.js
*
* Managed dynamic body, which moves but is not affected (directly) by the
* physics engine. This is not a true kinematic body, in the sense that we are
* letting the physics engine _compute_ collisions against it and selectively
* applying those collisions to the object. The physics engine does not decide
* the position/velocity/rotation of the element.
*
* Used for the camera object, because full physics simulation would create
* movement that feels unnatural to the player. Bipedal movement does not
* translate nicely to rigid body physics.
*
* See: http://www.learn-cocos2d.com/2013/08/physics-engine-platformer-terrible-idea/
* And: http://oxleygamedev.blogspot.com/2011/04/player-physics-part-2.html
*/
const EPS = 0.000001;
AFRAME.registerComponent('kinema-body', {
dependencies: ['velocity'],
/*******************************************************************
* Schema
*/
schema: {
mass: { default: 5 },
radius: { default: 1.3 },
linearDamping: { default: 0.05 },
enableSlopes: { default: true },
enableJumps: { default: false },
},
/*******************************************************************
* Lifecycle
*/
init: function () {
this.system = this.el.sceneEl.systems.physics;
this.system.addComponent(this);
const el = this.el,
data = this.data,
position = (new CANNON.Vec3()).copy(el.object3D.getWorldPosition(new THREE.Vector3()));
this.body = new CANNON.Body({
material: this.system.getMaterial('staticMaterial'),
position: position,
mass: data.mass,
linearDamping: data.linearDamping,
fixedRotation: true
});
this.body.addShape(
new CANNON.Sphere(data.radius),
new CANNON.Vec3(0, data.radius, 0)
);
this.body.el = this.el;
this.el.body = this.body;
this.system.addBody(this.body);
if (el.hasAttribute('wasd-controls')) {
console.warn('[kinema-body] Not compatible with wasd-controls, use movement-controls.');
}
},
remove: function () {
this.system.removeBody(this.body);
this.system.removeComponent(this);
delete this.el.body;
},
/*******************************************************************
* Update
*/
/**
* Checks CANNON.World for collisions and attempts to apply them to the
* element automatically, in a player-friendly way.
*
* There's extra logic for horizontal surfaces here. The basic requirements:
* (1) Only apply gravity when not in contact with _any_ horizontal surface.
* (2) When moving, project the velocity against exactly one ground surface.
* If in contact with two ground surfaces (e.g. ground + ramp), choose
* the one that collides with current velocity, if any.
*/
beforeStep: function (t, dt) {
if (!dt) return;
const el = this.el;
const data = this.data
const body = this.body;
if (!data.enableJumps) body.velocity.set(0, 0, 0);
body.position.copy(el.getAttribute('position'));
},
step: (function () {
const velocity = new THREE.Vector3(),
normalizedVelocity = new THREE.Vector3(),
currentSurfaceNormal = new THREE.Vector3(),
groundNormal = new THREE.Vector3();
return function (t, dt) {
if (!dt) return;
let body = this.body,
data = this.data,
didCollide = false,
height, groundHeight = -Infinity,
groundBody,
contacts = this.system.getContacts();
dt = Math.min(dt, this.system.data.maxInterval * 1000);
groundNormal.set(0, 0, 0);
velocity.copy(this.el.getAttribute('velocity'));
body.velocity.copy(velocity);
for (var i = 0, contact; contact = contacts[i]; i++) {
// 1. Find any collisions involving this element. Get the contact
// normal, and make sure it's oriented _out_ of the other object and
// enabled (body.collisionReponse is true for both bodies)
if (!contact.enabled) { continue; }
if (body.id === contact.bi.id) {
contact.ni.negate(currentSurfaceNormal);
} else if (body.id === contact.bj.id) {
currentSurfaceNormal.copy(contact.ni);
} else {
continue;
}
didCollide = body.velocity.dot(currentSurfaceNormal) < -EPS;
if (didCollide && currentSurfaceNormal.y <= 0.5) {
// 2. If current trajectory attempts to move _through_ another
// object, project the velocity against the collision plane to
// prevent passing through.
velocity.projectOnPlane(currentSurfaceNormal);
} else if (currentSurfaceNormal.y > 0.5) {
// 3. If in contact with something roughly horizontal (+/- 45º) then
// consider that the current ground. Only the highest qualifying
// ground is retained.
height = body.id === contact.bi.id
? Math.abs(contact.rj.y + contact.bj.position.y)
: Math.abs(contact.ri.y + contact.bi.position.y);
if (height > groundHeight) {
groundHeight = height;
groundNormal.copy(currentSurfaceNormal);
groundBody = body.id === contact.bi.id ? contact.bj : contact.bi;
}
}
}
normalizedVelocity.copy(velocity).normalize();
if (groundBody && (!data.enableJumps || normalizedVelocity.y < 0.5)) {
if (!data.enableSlopes) {
groundNormal.set(0, 1, 0);
} else if (groundNormal.y < 1 - EPS) {
groundNormal.copy(this.raycastToGround(groundBody, groundNormal));
}
// 4. Project trajectory onto the top-most ground object, unless
// trajectory is > 45º.
velocity.projectOnPlane(groundNormal);
} else if (this.system.driver.world) {
// 5. If not in contact with anything horizontal, apply world gravity.
// TODO - Why is the 4x scalar necessary.
// NOTE: Does not work if physics runs on a worker.
velocity.add(this.system.driver.world.gravity.scale(dt * 4.0 / 1000));
}
body.velocity.copy(velocity);
this.el.setAttribute('velocity', body.velocity);
this.el.setAttribute('position', body.position);
};
}()),
/**
* When walking on complex surfaces (trimeshes, borders between two shapes),
* the collision normals returned for the player sphere can be very
* inconsistent. To address this, raycast straight down, find the collision
* normal, and return whichever normal is more vertical.
* @param {CANNON.Body} groundBody
* @param {CANNON.Vec3} groundNormal
* @return {CANNON.Vec3}
*/
raycastToGround: function (groundBody, groundNormal) {
let ray,
hitNormal,
vFrom = this.body.position,
vTo = this.body.position.clone();
ray = new CANNON.Ray(vFrom, vTo);
ray._updateDirection(); // TODO - Report bug.
ray.intersectBody(groundBody);
if (!ray.hasHit) return groundNormal;
// Compare ABS, in case we're projecting against the inside of the face.
hitNormal = ray.result.hitNormalWorld;
return Math.abs(hitNormal.y) > Math.abs(groundNormal.y) ? hitNormal : groundNormal;
}
});
</script>
<body>
<a-scene physics="debug: true">
<a-box static-body position="0 0 0" height="3" width="4" color="red"></a-box>
<a-plane static-body position="0 0 0" rotation="-90 0 0" width="8" height="14" color="#7BC8A4"></a-plane>
<a-sky color="#ECECEC"></a-sky>
<a-entity kinema-body="radius: 0.8" movement-controls="fly: false" position="0 0 4" look-controls>
<a-entity camera position="0 1.6 0" ></a-entity>
</a-entity>
</a-scene>
</body>
</html>