Par Benoit, le 09/03/2018
Cela fait quelques temps que je souhaite tester les possibilités AR (augmented reality) en web pur. Je vous fais donc part de mon expérience.
Pour les modélisations 3D j'ai l'habitude d'utiliser blender (https://www.blender.org/), c'est un outil très puissant même si sa learning curve est un peu raide au démarrage. En effet, la modélisation pure, le texturing, en passant par le rendu photo réaliste et même l'animation, exigent de blender un très large éventail de fonctionnalités.
Pour le rendu web j'ai choisi AR.js (https://github.com/jeromeetienne/AR.js). Cette librairie javascript est au dessus de three.js (https://threejs.org/). Elle comporte de bonnes optimisations pour les plates-formes à ressources limitées telles que les smart-phone.
Il existe une librairie au dessus de three.js nommée a-frame https://aframe.io/. Elle permet de construire une scène en pseudo-html. Exemple:
<a-scene>
<a-box position="-1 0.5 -3" rotation="0 45 0" color="#4CC3D9"></a-box>
<a-sphere position="0 1.25 -5" radius="1.25" color="#EF2D5E"></a-sphere>
<a-cylinder position="1 0.75 -3" radius="0.5" height="1.5"
color="#FFC65D"></a-cylinder>
<a-plane position="0 0 -4" rotation="-90 0 0" width="4" height="4"
color="#7BC8A4"></a-plane>
<a-sky color="#ECECEC"></a-sky>
</a-scene>
Il est même possible de créer son propre tag, pour charger des éléments dans la scène. Mes essais avec cette librairie ne m'ont pas satisfait. Le FPS était trop bas sur la plate-forme de test (galaxy tab 4). J'ai donc supprimé cette couche pour utiliser directement ArToolkitSource de AR.js.
Enfin pour charger le modèle 3D avec les animations, j'ai choisi de passer par le format GLTF2 (https://github.com/KhronosGroup/glTF) un format libre pour les scènes 3D. Il faut savoir qu'il existe un add-on blender dans le dépôt three.js utilisant le format json. Mais celui-ci n'est plus activement maintenu, d'ailleurs les développeurs de three.js se demandent si cet addon doit rester dans le dépôt GIT de three.js (issue github). De mon coté j'ai choisi l'addon: glTF-Blender-Exporter
Voici un essai fonctionnant avec le marker HIRO
var container, stats, controls, clock, mixer;
var camera, scene, renderer, light;
var arToolkitContext;
var smoothedControls;
var clock = new THREE.Clock();
var onRenderFcts= [];
var SHADOW = false;
function initAR(){
var arToolkitSource = new THREEx.ArToolkitSource({
sourceType : 'webcam'
})
arToolkitSource.init(function onReady(){
onResize()
})
window.addEventListener('resize', function(){
onResize()
})
function onResize(){
arToolkitSource.onResize()
arToolkitSource.copySizeTo(renderer.domElement)
if( arToolkitContext.arController !== null ){
arToolkitSource.copySizeTo(arToolkitContext.arController.canvas)
}
}
// create atToolkitContext
arToolkitContext = new THREEx.ArToolkitContext({
cameraParametersUrl: THREEx.ArToolkitContext.baseURL +
'../data/data/camera_para.dat',
detectionMode: 'mono',
maxDetectionRate: 30,
canvasWidth: 80*3,
canvasHeight: 60*3,
})
// initialize it
arToolkitContext.init(function onCompleted(){
// copy projection matrix to camera
camera.projectionMatrix.copy( arToolkitContext.getProjectionMatrix() );
})
// update artoolkit on every frame
onRenderFcts.push(function(){
if( arToolkitSource.ready === false ) return
arToolkitContext.update( arToolkitSource.domElement )
})
var markerRoot = new THREE.Group
scene.add(markerRoot)
var artoolkitMarker = new THREEx.ArMarkerControls(arToolkitContext, markerRoot, {
type : 'pattern',
patternUrl : THREEx.ArToolkitContext.baseURL + '../data/data/patt.hiro'
})
// build a smoothedControls
var smoothedRoot = new THREE.Group()
scene.add(smoothedRoot)
smoothedControls = new THREEx.ArSmoothedControls(smoothedRoot, {
lerpPosition: 0.4,
lerpQuaternion: 0.3,
lerpScale: 1,
})
onRenderFcts.push(function(delta){
smoothedControls.update(markerRoot)
})
var arWorldRoot = smoothedRoot;
return arWorldRoot;
}
init();
function init() {
container = document.createElement( 'div' );
document.body.appendChild( container );
scene = new THREE.Scene();
light = new THREE.DirectionalLight( 0xffffff ,2);
light.position.set( 20, 20, 20 );
if(SHADOW){
light.castShadow = true;
light.shadow.mapSize.width = 512; // default
light.shadow.mapSize.height = 512; // default
light.shadow.camera.near = 0.5; // default
light.shadow.camera.far = 500 // default
}
camera = new THREE.PerspectiveCamera();
var loader = new THREE.GLTF2Loader();
loader.load( 'tram2.gltf', function ( gltf ) {
for(var index in gltf.scene.children){
var obj = gltf.scene.children[index];
if(SHADOW){
if(obj.name == "Plane"){
obj.castShadow = false;
obj.receiveShadow = true;
}else{
obj.castShadow = true;
obj.receiveShadow = true;
}
}
}
scene.add(camera);
console.debug("camera", gltf.cameras)
console.debug("camera", camera )
console.debug("gltf", gltf)
console.debug("scene", scene)
root = initAR();
var geometry = new THREE.CubeGeometry(1,1,1);
var material = new THREE.MeshNormalMaterial({
transparent : true,
opacity: 0.5,
side: THREE.DoubleSide
});
var mesh = new THREE.Mesh( geometry, material );
root.add( light );
root.add( gltf.scene );
var animationClip = gltf.animations[ 0 ];
mixer = new THREE.AnimationMixer( scene );
mixer.clipAction( animationClip ).play();
animate();
} );
renderer = new THREE.WebGLRenderer({
antialias: true,
alpha: true
});
renderer.setPixelRatio( window.devicePixelRatio );
renderer.setSize( window.innerWidth, window.innerHeight );
renderer.gammaOutput = true;
if(SHADOW){
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
}
container.appendChild( renderer.domElement );
window.addEventListener( 'resize', onWindowResize, false );
stats = new Stats();
container.appendChild( stats.dom );
}
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize( window.innerWidth, window.innerHeight );
}
var lastTimeMsec= null;
function animate(nowMsec) {
requestAnimationFrame( animate );
renderer.render( scene, camera );
stats.update();
lastTimeMsec = lastTimeMsec || nowMsec-1000/60
var deltaMsec = Math.min(200, nowMsec - lastTimeMsec)
lastTimeMsec = nowMsec
onRenderFcts.forEach(function(onRenderFct){
onRenderFct(deltaMsec/1000, nowMsec/1000)
})
render()
}
function render() {
var delta = 0.75 * clock.getDelta();
mixer.update( delta );
renderer.render( scene, camera );
}
Benoit