본문 바로가기

프로그래밍/Web

WebXR + three.js

XR

navigator.xr

지원여부 검토
if( navigator.xr && XRSession.prototype.requestHitTest ) {
  // XRDevice 얻기
} else {
 // 미지원 브라우저
}

 

 

XRDevice

// XRDevice  얻기
this.device = await navigator.xr.requestDevice();

// canvas 생성하고, 출력을 위한 xr 컨텍스트를 얻는다.
var outputCanvas = document.createElement('canvas');
var ctx = outputCanvas.getContext('xrpresent');

 

 

XRSession

// XRSession 얻기
const session = await this.device.requestSession( { 
  environmentIntegration: true, 
  outputContext: ctx
});



// 세션이 정상적으로 생성되었으면 캔버스를 body에 추가한다.
document.body.appendChild(outputCanvas);
this.session = session;

 

 

 

THREE web gl 설정

// WebGLRenderer 생성
this.renderer = new THREE.WebGLRenderer( {
                       alpha: true,
                       preserveDrawingBuffer: true,
                       });

this.renderer.autoClear = false;
this.renderer.shadowMap.enabled = true;
this.renderer.shadowMap.type = THREE.PCFSoftShadowMap;

// 렌더러의 컨텍스트 정보 저장
this.gl = this.renderer.getContext();
await this.gl.setCompatibleXRDevice( this.session.device );


// 세션의 기본 레이어를 생성한다.
session.baseLayer = new XRWebGLLayer( this.session, this.gl );


// 바인딩된 프레임버퍼가 변경되는 증상이 있어, 가끔 모델들이 사라져버림
// three.js 오브젝트 렌더전에 프레임 버퍼를 다시 바인딩 하도록 설정
// 
THREE.Object3D.prototype.onBeforeRender = ()=> {
  this.gl.bindFramebuffer( this.gl.FRAMEBUFFER, this.session.baseLayer.framebuffer );
});



// Scene 구성, 모델 로드 등 필요한 처리
this.clock = new THREE.Clock();
this.objectArray = [];
this.updateFunctionArray=[];
this.scene = new THREE.Scene();


// light
const light = new THREE.AmbientLight(0xffffff, 1 );
const directionalLight = new THREE.DirectionalLight( 0xffffff );
directionalLight.position.set( 10, 15, 10 );
this.scene.add( light );
this.scene.add( directionalLight );



// fbx 로더
var loader = new THREE.FBXLoader();
loader.load( 'test.fbx', object => {

  if( object.animations != null ) {
    if( object.animations.length > 0 ) {
      object.mixer = new THREE.AnimationMixer( object );
      var action = object.mixer.clipAction( object.animations[0]);
      action.play();
    }
  }

  object.traverse( child => {
    if( child.isMesh ) {
      child.castShadow = true;
      child.receiveShadow = true;
      
      // texture
      var textureLoader = new THREE.TextureLoader();
      
      // custom texture
      var mat = new THREE.MeshStandardMaterial( {
                            roughness: 0.0,
                            metalness: 0.0,
                            map: textureLoader.load( "url" ),
                            skinning: true,
                            side : THREE.DoubleSide
                            });
      child.material = mat;

    }

  });


  this.objectArray.push(object);
}, ()=>{}, ()=>{} );



// 애니메이션 믹서를 업데이트하기 위해 업데이트 함수를 등록해 준다.
this. updateFunctionArray.push(  ( delta ) => {
  if( this.objectArray.length > 0 ) {
    for( var i=0; i<this.objectArray.length; i++ ) {
      let mixer = this.objectArray[i];
      if( mixer ) {
        mixer.update( delta );
      }
    }
  }
});



// 카메라
this.camera = new THREE.PerspectiveCamera();
this.camera.matrixAutoUpdate = false;

// 기준이 될 프레임 정보
this.frameOfRef = await this.session.requestFrameOfReference('eye-level');

// 매 프레임 호출할 콜백 함수를 지정한다.
this.session.requestAnimationFrame( this.onXRFrame );

// click
window.addEventListener( 'click', this.onClick );

 

 

 

Frame Callback

onXRFrame( time, frame ) {
  let session = frame.session;
  let pose = frame.getDevicePose( this.frameOfRef );

  // 프레임버퍼를 세션의 프레임버퍼로 연결한다.
  this.gl.bindFramebuffer( this.gl.FRAMEBUFFER, this.session.baseLayer.framebuffer);

  // 등록한 업데이트 함수들을 호출
  var delta = this.clock.getDelta();
  this. updateFunctionArray.forEach( ( updateFunction ) => {
    updateFunction(delta);
  });



  if( pose ) {
    // frame의 뷰들을 가져와 처리한다.
    // vr 의 경우 좌우 스테레오이기에 2개의 뷰가 있고, ar은 1개의 뷰를 가진다.
    for( let view of frame.views ) {
      // 뷰포트에 맞게 렌더러 사이즈 설정
      const viewport = session.baseLayer.getViewport( view );
      this.renderer.setSize(viewport.width, viewport.height);

      // 뷰의 프로젝션 행렬을 카메라에 맞춤
      this.camera.projectionMatrix.fromArray(view.projectionMatrix);

      // 포즈에 대한 뷰 행렬 얻기
      const viewMatrix = new THREE.Matrix4().fromArray( pose.getViewMatrix(view));

      // 뷰행렬 적용
      this.camera.matrix.getInverse(viewMatrix);
      this.camera.updateMatrixWorld(true);

      // 테스트 중 아래 루틴 추가 시 카메라 영상이 표시되지 않는 문제 발생됨. 
      // this.renderer.clearDepth();

      // 렌더링
      this.renderer.render( this.scene, this.camera );
    }

  }

  // 반복
  session.requestAnimationFrame( this.onXRFrame );
}

 

 

 

클릭 이벤트
async onClick( e ) {
  // 아래와 같이 object3d 객체를 가져와 원하는 처리를 추가
  let model = this.objectArray[0];
  const x = 0;
  const y = 0;
  this.raycaster = this.raycaster || new THREE.Raycaster();
  this.raycaster.setFromCamera( , this.camera);
  const ray = this.raycaster.ray;

  const origin = new Float32Array(ray.origin.toArray());
  const direction = new Float32Array(ray.direction.toArray());

  const hits = await this.session.requestHitTest( origin, direction, this.frameOfRef);

  if( hits.length ) {
    const hit = hits[0];
    const hitMatrix = new THREE.Matrix4().fromArray(hit.hitMatrix);
    model.position.setFromMatrixPosition(hitMatrix);
    DemoUtils.lookAtOnY( model, this.camera);

    const shadowMesh = this.scene.children.find(c => c.name === 'shadowMesh');
    shadowMesh.position.y = model.position.y;

    this.scene.add(model);
  }
}

 

'프로그래밍 > Web' 카테고리의 다른 글

[tauri] 테스트용 앱 기본 구성  (0) 2024.05.03
리액트 주요 요소  (0) 2024.04.24
[tauri] 개발환경  (0) 2024.03.19
[JavaScript] Promise, async/await  (0) 2018.08.17
javascript 모듈화 패턴  (0) 2018.07.26
VSCode 웹 디버깅  (0) 2018.07.19
openssl  (0) 2018.07.17
Windows 레드마인 설치(Bitnami)  (1) 2017.09.22
Gradle Wrapper  (0) 2017.03.06
스프링 요청/응답  (0) 2017.02.26