見出し画像

Three.js 0.166.0 & Rapier 0.13.1 を使って物理演算してみるテストその3


概要

前回からのつづきです。 前回前々回の実験での使えそうな機能をまとめて、3Dモデルがフィールド上で物理演算で動けるようにしてみました。 

その実装コード3種類へのリンクをこのnoteに載せています。 どのコードでも共通の操作で、「Left」「Forward」「Backward」「Right」ボタンで操作対象3Dモデルを、左回転、前進、後退、右回転させます。 PCのキーボードの←↑↓→キーでも同じ操作ができます。 また「camLeft」「camRight」「camHeight」「camDist」ボタンも共通機能としてカメラを、左回転、右回転、高さ変更、距離変更、できます。 PCのキーボードのADWSキーでも同じ操作ができます。 操作中の操作対象が「狐」3Dモデルに衝突した場合は「狐」3Dモデルが移動します。 「ロボ」3Dモデルは操作対象が衝突しても動きません。

最初のPractice32のコードはvelocityを使用して移動するコードです。 ボタンを離しても少し移動が続いてしまいます。 

次のPractice32_02のコードはsetTranslationを使用して移動するコードです。 ボタンを離しても「移動」が残らず、こちらの動きのほうが自分の好みです。

最後のPractice32_03のコードはsetTranslationを使用しておまけで作りましたw。 setTranslationでの一度の移動距離を大きめにして、漫画やアニメなどで見かける高速移動のようなものを表現してみたつもりですw。 何かの漫画で見かけた「瞬歩」という言葉を思いだし、その言葉を頭の中で繰り返ししながらこの意味のない処理を実行していましたw。 まぁ、はっきり言って自分趣味のいらないコードですw。 

自分としてはPractice32とPractice32_02は、ちょっとした完成形となったので、長いですが全コード載せています。

コード作成にあたり本家サイトから以下サイトを参考にさせていただきました
Rapier
dimforge / rapier
Rigid-bodies

前に作成した以下の自分のサイトやコードも参考にしました
Three.js r110とAmmo.jsを併用して物理演算してみるテストその3
Three.js r110とAmmo.js(20200314時点)によるglTF3Dモデルの表示&操作その06
Three.js 0.166.0 & Rapier 0.13.1 Practice 20 Mutiple glTF Model Animations with Physics
Three.js 0.166.0 & Rapier 0.13.1 Practice 21 Mutiple glTF Model Animations02 with Physics
Three.js 0.166.0 & Rapier 0.13.1 Practice 22 Mutiple glTF Model Animations03 with Physics

Practice 32 Camera Rotation & 3DModel Move by velocity

Three.js 0.166.0 & Rapier 0.13.1 Practice 32 Camera Rotation & 3DModel Move by velocity

HTML

<script type="importmap">
    {
        "imports": {
            "three": "https://esm.sh/three@0.166.0",
            "three/addons/": "https://esm.sh/three@0.166.0/examples/jsm/",
            "@dimforge/rapier3d-compat": "https://esm.sh/@dimforge/rapier3d-compat@0.13.1"
        }
    }
</script>


<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/purecss@3.0.0/build/pure-min.css" integrity="sha384-X38yfunGUhNzHpBaEBsWLO+A0HDYOQi8ufWDkZ0k9e0eXz/tH3II7uKZ9msv++Ls" crossorigin="anonymous">

<div class="pure-g">

    <div class="pure-u-1-2">

        <button class="pure-button pure-button-primary" id="leftButton">Left</button>
        <button class="pure-button pure-button-primary" id="forwardButton">Forward</button>
        <button class="pure-button pure-button-primary" id="backwardButton">Backward</button>
        <button class="pure-button pure-button-primary" id="rightButton">Right</button>

    </div>

    <div class="pure-u-1-2">

        <button class="pure-button pure-button-primary" id="camLeftButton">camLeft</button>
        <button class="pure-button pure-button-primary" id="camRightButton">camRight</button>
        <button class="pure-button pure-button-primary" id="camHeightButton">camHeight</button>
        <button class="pure-button pure-button-primary" id="camDistanceButton">camDist</button>

    </div>

</div>

CSS

body {
    overflow: hidden;
    margin: 0px;
}

JavaScript

import * as THREE from 'three'
import { OrbitControls } from 'three/addons/controls/OrbitControls.js'
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'
import RAPIER from '@dimforge/rapier3d-compat'

class RapierDebugRenderer {
    mesh
    world
    enabled = true

    constructor(scene, world) {
        this.world = world
        this.mesh = new THREE.LineSegments(new THREE.BufferGeometry(), new THREE.LineBasicMaterial({ color: 0xffffff, vertexColors: true }))
        this.mesh.frustumCulled = false
        scene.add(this.mesh)
    }

    update() {
        if (this.enabled) {
            const { vertices, colors } = world.debugRender()
            this.mesh.geometry.setAttribute('position', new THREE.BufferAttribute(vertices, 3))
            this.mesh.geometry.setAttribute('color', new THREE.BufferAttribute(colors, 4))
            this.mesh.visible = true
        } else {
            this.mesh.visible = false
        }
    }
}


// カメラの高さに関する変数・定数の設定
// 高さの最小値
const MIM_CAMERA_HEIGHT = 10;
// 高さの最大値
const MAX_CAMERA_HEIGHT = 25;
// 高さの初期値
const DEFFAULT_CAMERA_HEIGHT = 15;
// 高さの計算単位
const CAMERA_HEIGHT_CALC_UNIT = 5;

// カメラの操作対象3Dオブジェクトからの距離に関する変数・定数の設定
// 距離の最小値
const MIM_CAMERA_DIST = 10;
// 距離の最大値
const MAX_CAMERA_DIST = 40;
// 距離の初期値
const DEFFAULT_CAMERA_DIST = 30;
// 距離の計算単位
const CAMERA_DIST_CALC_UNIT = 10;
// 距離の初期値を設定
let cameraDistance = DEFFAULT_CAMERA_DIST;

// 操作用3Dオブジェクトの座標初期値
// → 表示用の3Dモデルと連動するRapier剛体(Cuboid)設定用
const OP_OBJECT_DEFAULT_POS_X = 0;
const OP_OBJECT_DEFAULT_POS_Y = 5;
const OP_OBJECT_DEFAULT_POS_Z = 2;



// 操作対象3Dオブジェクトの回りを回転するカメラの位置を計算用
let rot = 0; // 角度
let targetRot = 0;

// 物理演算を行うRapierを初期化
// Rapier物演算での重力、その重力による物理演算用の world を作成
await RAPIER.init() // This line is only needed if using the compat version
const gravity = new RAPIER.Vector3(0.0, -9.81, 0.0)
const world = new RAPIER.World(gravity)

// Three.jsによる表示のためのSceneを作成
const scene = new THREE.Scene()
scene.background = new THREE.Color( 0xbfd1e5 );
// 遠方をぼやけさせるためのfogを設定
scene.fog = new THREE.FogExp2( 0xcce0ff, 0.015 );

// 3Dモデルの操作用
let leftRotation = false
let forward = false
let backward = false
let rightRotation = false

// カメラの操作用
let camLeftRotation = false
let camRightRotation = false


// Rapierデバッグ表示用
const rapierDebugRenderer = new RapierDebugRenderer(scene, world)


// アニメーション処理用
let mixer     // glTF形式のアニメーション付きソルジャー3Dモデル用
let mixer02   // glTF形式のアニメーション付きロボット3Dモデル用
let mixer03   // glTF形式のアニメーション付きキツネ3Dモデル用


// 3Dエンジンのライト(HemisphereLight)を設定
let hemiLight = new THREE.HemisphereLight( 0xffffff, 0xffffff, 1 );
scene.add( hemiLight );

// 3Dエンジンのライト(DirectionalLight)を設定
let dirLight = new THREE.DirectionalLight( 0xffffff , 1);
scene.add( dirLight );

// 3Dエンジンのカメラを設定
const camera = new THREE.PerspectiveCamera( 30, window.innerWidth/window.innerHeight, 0.2, 5000 ); 

// 操作用3Dモデルの座標の初期値をもとに、カメラの postion.x と posision.y の座標を初期化
camera.position.x = OP_OBJECT_DEFAULT_POS_X + cameraDistance * Math.sin(rot * Math.PI / 180);
camera.position.y = DEFFAULT_CAMERA_HEIGHT; 
camera.position.z = OP_OBJECT_DEFAULT_POS_Z + cameraDistance * Math.cos(rot * Math.PI / 180);

// WebGLレンダラを作成し設定
const renderer = new THREE.WebGLRenderer({ antialias: true })
renderer.setSize(window.innerWidth, window.innerHeight)
renderer.shadowMap.enabled = true
renderer.shadowMap.type = THREE.VSMShadowMap
document.body.appendChild(renderer.domElement)

// 画面サイズ変更時にカメラ、レンダラーを再設定
window.addEventListener('resize', () => {
    camera.aspect = window.innerWidth / window.innerHeight
    camera.updateProjectionMatrix()
    renderer.setSize(window.innerWidth, window.innerHeight)
})



// 地面用の3Dモデルと物理演算用の剛体を作成

// 地面用のテクスチャを読み込み
let groundMaterial
const groundMaterialLoader = async () => {

    let loader = new THREE.TextureLoader();
    let groundTexture = loader.load( 'https://rawcdn.githack.com/mrdoob/three.js/79edf22a345079dc6cf5d8c6ad38ee22e9edab3c/examples/textures/terrain/grasslight-big.jpg' );
    groundTexture.wrapS = groundTexture.wrapT = THREE.RepeatWrapping;
    groundTexture.repeat.set( 25, 25 );
    groundTexture.anisotropy = 16;
    groundTexture.encoding = THREE.sRGBEncoding;
    groundMaterial = new THREE.MeshLambertMaterial( { map: groundTexture } );

}
await groundMaterialLoader()

// 地面の表示用3Dモデル(Box)作成
// Three.jsのBoxオブジェクトによる3Dモデルを作成し、上記で読み込んだ地面用テクスチャを設定
const floorMesh = new THREE.Mesh( new THREE.BoxGeometry( 2000, 1, 2000 ), groundMaterial )
floorMesh.position.y = -1
scene.add(floorMesh)

// 地面の表示用Box3Dモデルに連動する、Rapierによる物理演算用の剛体(Cuboid)を作成
const floorBody = world.createRigidBody(RAPIER.RigidBodyDesc.fixed().setTranslation(0, -1, 0))
const floorShape = RAPIER.ColliderDesc.cuboid( 1000, 0.5, 1000 )
world.createCollider(floorShape, floorBody)



// ソルジャーの表示用の3Dモデルと物理演算用の剛体を作成
// Three.jsのGLTFLoaderでアニメーション付きのソルジャーー3Dモデルを読み込み表示
const gltf = await new GLTFLoader().loadAsync( 'https://cdn.jsdelivr.net/gh/mrdoob/three.js@r166/examples/models/gltf/Soldier.glb' )
const gltfMesh = gltf.scene
gltfMesh.scale.set( 3, 3, 3 )

// 複数あるソルジャーー3Dモデルのアニメーションから「Walk」を設定
let animations = gltf.animations
if ( animations && animations.length ) {
    mixer = new THREE.AnimationMixer( gltf.scene );
    let animation = THREE.AnimationClip.findByName( animations, 'Walk' ) // Set animation name to play a specific animation
    mixer.clipAction( animation ).play() // Play a specific animation
}

scene.add( gltfMesh )

// 上記ソルジャー3Dモデルに連動するRapierによる物理演算用の剛体(Cuboid)を作成
//const gltfBody = world.createRigidBody(RAPIER.RigidBodyDesc.dynamic().setTranslation(0, 5, 2).setCanSleep(false))
const gltfBody = world.createRigidBody(RAPIER.RigidBodyDesc.dynamic().setTranslation(OP_OBJECT_DEFAULT_POS_X, OP_OBJECT_DEFAULT_POS_Y, OP_OBJECT_DEFAULT_POS_Z).setCanSleep(false))
gltfBody.setEnabledRotations(false, true, false, true)
const gltfShape = RAPIER.ColliderDesc.cuboid(1.5, 2.5, 1.5).setMass(1).setRestitution(0.5)
world.createCollider(gltfShape, gltfBody)



// ロボットの表示用の3Dモデルと物理演算用の剛体を作成
// Three.jsのGLTFLoaderでアニメーション付きのロボット3Dモデルを読み込み表示
const gltf02 = await new GLTFLoader().loadAsync( 'https://cdn.jsdelivr.net/gh/mrdoob/three.js@r166/examples/models/gltf/RobotExpressive/RobotExpressive.glb' )
const gltfMesh02 = gltf02.scene
gltfMesh02.scale.set( 1, 1, 1 )

// 複数あるロボット3Dモデルのアニメーションから「Idle」を設定
animations = gltf02.animations
if ( animations && animations.length ) {
    mixer02 = new THREE.AnimationMixer( gltf02.scene );
    let animation = THREE.AnimationClip.findByName( animations, 'Idle' ) // Set animation name to play a specific animation
    mixer02.clipAction( animation ).play() // Play a specific animation
}

scene.add( gltfMesh02 )

// 上記ロボット3Dモデルに連動するRapierによる物理演算用の剛体(Cuboid)を作成
const gltfBody02 = world.createRigidBody(RAPIER.RigidBodyDesc.fixed().setTranslation(5, 2.2, -10).setCanSleep(false))
const gltfShape02 = RAPIER.ColliderDesc.cuboid(1.5, 2.2, 1.5).setMass(1).setRestitution(0.5)
world.createCollider(gltfShape02, gltfBody02)



// キツネの表示用の3Dモデルと物理演算用の剛体を作成
// Three.jsのGLTFLoaderでアニメーション付きのキツネ3Dモデルを読み込み表示
const gltf03 = await new GLTFLoader().loadAsync( 'https://rawcdn.githack.com/KhronosGroup/glTF-Sample-Models/8e9a5a6ad1a2790e2333e3eb48a1ee39f9e0e31b/2.0/Fox/glTF-Binary/Fox.glb' )
//const gltfMesh03 = gltf03.scene.children[ 0 ]
const gltfMesh03 = gltf03.scene
gltfMesh03.scale.set( 0.04, 0.04, 0.04 )

// 複数あるキツネ3Dモデルのアニメーションから「Survey」を設定
animations = gltf03.animations
if ( animations && animations.length ) {
    mixer03 = new THREE.AnimationMixer( gltf03.scene );
    let animation = THREE.AnimationClip.findByName( animations, 'Survey' ) // Set animation name to play a specific animation
    mixer03.clipAction( animation ).play() // Play a specific animation
}

// 上記キツネ3Dモデルに連動するRapierによる物理演算用の剛体(Cuboid)を作成
scene.add( gltfMesh03 )
const gltfBody03 = world.createRigidBody(RAPIER.RigidBodyDesc.dynamic().setTranslation(7, 0, 4).setCanSleep(false))
gltfBody03.setEnabledRotations(false, true, false, true)
const gltfShape03 = RAPIER.ColliderDesc.cuboid(0.75, 2, 2).setMass(1).setRestitution(0.5)
world.createCollider(gltfShape03, gltfBody03)



// 赤玉の表示用の3Dモデルと物理演算用の剛体を作成
// Three.jsのSphereオブジェクトによる3Dモデルを表示
const sphereMesh = new THREE.Mesh(new THREE.SphereGeometry(), new THREE.MeshBasicMaterial({color: 0x770000}))
sphereMesh.castShadow = true
scene.add(sphereMesh)

// 上記Sphere3Dモデルに連動するRapierによる物理演算用の剛体(Ball)を作成
const sphereBody = world.createRigidBody(RAPIER.RigidBodyDesc.dynamic().setTranslation(0, 5, -3).setCanSleep(false))
const sphereShape = RAPIER.ColliderDesc.ball(1).setMass(1).setRestitution(0.5)
world.createCollider(sphereShape, sphereBody)



// 3Dモデル操作用ボタン処理
// 左回転
document.querySelector('#leftButton').onpointerdown = () => leftRotation = true
document.querySelector('#leftButton').onpointerup = () => leftRotation = false
document.querySelector('#leftButton').onpointerleave = () => leftRotation = false

// 前進
document.querySelector('#forwardButton').onpointerdown = () => forward = true
document.querySelector('#forwardButton').onpointerup = () => forward = false
document.querySelector('#forwardButton').onpointerleave = () => forward = false

// 後退
document.querySelector('#backwardButton').onpointerdown = () => backward = true
document.querySelector('#backwardButton').onpointerup = () => backward = false
document.querySelector('#backwardButton').onpointerleave = () => backward = false

// 右回転
document.querySelector('#rightButton').onpointerdown = () => rightRotation = true
document.querySelector('#rightButton').onpointerup = () => rightRotation = false
document.querySelector('#rightButton').onpointerleave = () => rightRotation = false

// カメラ操作用ボタン処理
// カメラの回転を操作
document.querySelector('#camLeftButton').onpointerdown = () => camLeftRotation = true
document.querySelector('#camLeftButton').onpointerup = () => camLeftRotation = false
document.querySelector('#camLeftButton').onpointerleave = () => camLeftRotation = false

// カメラの回転を操作
document.querySelector('#camRightButton').onpointerdown = () => camRightRotation = true
document.querySelector('#camRightButton').onpointerup = () => camRightRotation = false
document.querySelector('#camRightButton').onpointerleave = () => camRightRotation = false

// カメラの高さを操作
document.querySelector('#camHeightButton').onpointerdown = () => {
    camera.position.y = camera.position.y + CAMERA_HEIGHT_CALC_UNIT;
    if ( camera.position.y > MAX_CAMERA_HEIGHT ) {
        camera.position.y = MIM_CAMERA_HEIGHT
    }
}

// カメラの距離を操作
document.querySelector('#camDistanceButton').onpointerdown = () => {
    cameraDistance = cameraDistance - CAMERA_DIST_CALC_UNIT;
    if ( cameraDistance < MIM_CAMERA_DIST ){
        cameraDistance = MAX_CAMERA_DIST
    }
}



// キー入力・リリース処理の設定
document.addEventListener("keydown", (event) => keyDownHandler(event), false);
document.addEventListener("keyup", (event) => keyUpHandler(event), false);

// 操作用キー入力処理
const keyDownHandler = (event) => {

    // 3Dモデル操作用キー入力処理
    // 左回転
    if ( event.key === "ArrowLeft" ) {
        leftRotation = true
    // 前進
    } else if ( event.key === "ArrowUp" ) {
        forward = true
    // 後退
    } else if ( event.key === "ArrowDown" ) {
        backward = true
    // 右回転
    } else if ( event.key === "ArrowRight" ) {
        rightRotation = true

    // カメラ操作用キー入力処理
    // カメラの回転を操作
    } else if ( event.key === "a" ) {
        camLeftRotation = true
    // カメラの回転を操作
    } else if ( event.key === "d" ) {
        camRightRotation = true
    // カメラの高さを操作
    } else if ( event.key === "w" ) {
        camera.position.y = camera.position.y + CAMERA_HEIGHT_CALC_UNIT;
        if ( camera.position.y > MAX_CAMERA_HEIGHT ) {
            camera.position.y = MIM_CAMERA_HEIGHT
    }
    // カメラの距離を操作
    } else if ( event.key === "s" ) {
        cameraDistance = cameraDistance - CAMERA_DIST_CALC_UNIT;
        if ( cameraDistance < MIM_CAMERA_DIST ){
            cameraDistance = MAX_CAMERA_DIST
        }
    }

}

// 操作用キーリリース処理
const keyUpHandler = (event) => {

    // 3Dモデル操作用キーリリース処理
    // 左回転の停止
    if ( event.key === "ArrowLeft" ) {
        leftRotation = false
    // 前進の停止
    } else if ( event.key === "ArrowUp" ) {
        forward = false
    // 後退の停止
    } else if ( event.key === "ArrowDown" ) {
        backward = false
    // 右回転の停止
    } else if ( event.key === "ArrowRight" ) {
        rightRotation = false;

    // カメラ操作用キーリリース処理
    // カメラの回転を停止
    } else if ( event.key === "a" ) {
        camLeftRotation = false
    // カメラの回転を停止
    } else if ( event.key === "d" ) {
        camRightRotation = false
    }

}


// 時間設定用
const clock = new THREE.Clock()
let delta

const animate = () => {

    requestAnimationFrame(animate)

    // 物理演算のステップを進める
    delta = clock.getDelta()
    world.timestep = Math.min(delta, 0.01)
    world.step()

    // when one of buttons is pressed, apply impulse to physical cube body (cuboid)
    // state of button is changed when button down or up 

    // 操作3Dモデルと連動する剛体の左回転処理
    if ( leftRotation ) {
        gltfBody.setAngvel({ x: 0.0, y: 2.0, z: 0.0 }, true)
    }

    // 操作3Dモデルと連動する剛体の前進処理
    if ( forward ) {
        const vector = new THREE.Vector3( 0, 0, -10 )
        vector.applyQuaternion( gltfMesh.quaternion )

        gltfBody.setLinvel({ x: vector.x, y: vector.y, z: vector.z }, true)
    }

    // 操作3Dモデルと連動する剛体の後退処理
    if ( backward ) {
        const vector = new THREE.Vector3( 0, 0, 10 )
        vector.applyQuaternion( gltfMesh.quaternion )

        gltfBody.setLinvel({ x: vector.x, y: vector.y, z: vector.z }, true)
    }

    // 操作3Dモデルと連動する剛体の後退転処理
    if ( rightRotation ) {
        gltfBody.setAngvel({ x: 0.0, y: -2.0, z: 0.0 }, true)
    }


    // カメラ操作の計算・設定その1 START 

    // カメラ左回転ボタン押下時の計算処理
    if(camLeftRotation) {
        targetRot = targetRot - 2
    // カメラ右回転ボタン押下時の計算処理
    } else if ( camRightRotation ) {
        targetRot = targetRot + 2
    }

    //rot += 0.5; // 毎フレーム角度を0.5度ずつ足していく
    // イージングの公式を用いて滑らかにする
    // 値 += (目標値 - 現在の値) * 減速値
    rot += (targetRot - rot) * 0.05

    // カメラ操作の計算・設定その1 END 


    // 物理演算した剛体の座標と回転を連動する表示用3Dモデルに反映 START 

    // ソルジャー3Dモデルの設定
    gltfMesh.position.copy(gltfBody.translation())
    gltfMesh.quaternion.copy(gltfBody.rotation())
    // glTFオブジェクトの座標と回転を表示用に調整
    gltfMesh.position.y = gltfMesh.position.y - 2.5

    // ロボット3Dモデルの設定
    gltfMesh02.position.copy(gltfBody02.translation())
    gltfMesh02.quaternion.copy(gltfBody02.rotation())
    // glTFオブジェクトの座標と回転を表示用に調整
    gltfMesh02.position.y = gltfMesh02.position.y - 2.2

    // キツネ3Dモデルの設定
    gltfMesh03.position.copy(gltfBody03.translation())
    gltfMesh03.quaternion.copy(gltfBody03.rotation())
    // glTFオブジェクトの座標と回転を表示用に調整
    gltfMesh03.position.y = gltfMesh03.position.y - 2
    gltfMesh03.rotation.y = gltfMesh03.rotation.y + Math.PI

    // 赤球の設定
    sphereMesh.position.copy(sphereBody.translation())
    sphereMesh.quaternion.copy(sphereBody.rotation())

    // 物理演算した剛体の座標と回転を連動する表示用3Dモデルに反映 END 


    // デバッグの描画を更新
    rapierDebugRenderer.update()


    // カメラ操作の計算・設定その2 START

    // 角度に応じてカメラの位置を設定
    camera.position.x = gltfMesh.position.x + cameraDistance * Math.sin(rot * Math.PI / 180)
    camera.position.z = gltfMesh.position.z + cameraDistance * Math.cos(rot * Math.PI / 180)
    // 操作対象3Dオブジェクトを見つめる
    camera.lookAt(new THREE.Vector3( gltfMesh.position.x, gltfMesh.position.y, gltfMesh.position.z ))

    // カメラ操作の計算・設定その2 END


    // 3Dモデルのアニメーションを更新
    if (mixer) mixer.update(delta)
    if (mixer02) mixer02.update(delta)
    if (mixer03) mixer03.update(delta)


    renderer.render(scene, camera)

}

animate()


Practice 32_02 Camera Rotation & 3DModel Move by setTranslation

Three.js 0.166.0 & Rapier 0.13.1 Practice 32_02 Camera Rotation & 3DModel Move by setTranslation

HTML

<script type="importmap">
    {
        "imports": {
            "three": "https://esm.sh/three@0.166.0",
            "three/addons/": "https://esm.sh/three@0.166.0/examples/jsm/",
            "@dimforge/rapier3d-compat": "https://esm.sh/@dimforge/rapier3d-compat@0.13.1"
        }
    }
</script>


<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/purecss@3.0.0/build/pure-min.css" integrity="sha384-X38yfunGUhNzHpBaEBsWLO+A0HDYOQi8ufWDkZ0k9e0eXz/tH3II7uKZ9msv++Ls" crossorigin="anonymous">

<div class="pure-g">

    <div class="pure-u-1-2">

        <button class="pure-button pure-button-primary" id="leftButton">Left</button>
        <button class="pure-button pure-button-primary" id="forwardButton">Forward</button>
        <button class="pure-button pure-button-primary" id="backwardButton">Backward</button>
        <button class="pure-button pure-button-primary" id="rightButton">Right</button>

    </div>

    <div class="pure-u-1-2">

        <button class="pure-button pure-button-primary" id="camLeftButton">camLeft</button>
        <button class="pure-button pure-button-primary" id="camRightButton">camRight</button>
        <button class="pure-button pure-button-primary" id="camHeightButton">camHeight</button>
        <button class="pure-button pure-button-primary" id="camDistanceButton">camDist</button>

    </div>

</div>

CSS

body {
    overflow: hidden;
    margin: 0px;
}

JavaScript


import * as THREE from 'three'
import { OrbitControls } from 'three/addons/controls/OrbitControls.js'
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'
import RAPIER from '@dimforge/rapier3d-compat'

class RapierDebugRenderer {
    mesh
    world
    enabled = true

    constructor(scene, world) {
        this.world = world
        this.mesh = new THREE.LineSegments(new THREE.BufferGeometry(), new THREE.LineBasicMaterial({ color: 0xffffff, vertexColors: true }))
        this.mesh.frustumCulled = false
        scene.add(this.mesh)
    }

    update() {
        if (this.enabled) {
            const { vertices, colors } = world.debugRender()
            this.mesh.geometry.setAttribute('position', new THREE.BufferAttribute(vertices, 3))
            this.mesh.geometry.setAttribute('color', new THREE.BufferAttribute(colors, 4))
            this.mesh.visible = true
        } else {
            this.mesh.visible = false
        }
    }
}


// カメラの高さに関する変数・定数の設定
// 高さの最小値
const MIM_CAMERA_HEIGHT = 10;
// 高さの最大値
const MAX_CAMERA_HEIGHT = 25;
// 高さの初期値
const DEFFAULT_CAMERA_HEIGHT = 15;
// 高さの計算単位
const CAMERA_HEIGHT_CALC_UNIT = 5;

// カメラの操作対象3Dオブジェクトからの距離に関する変数・定数の設定
// 距離の最小値
const MIM_CAMERA_DIST = 10;
// 距離の最大値
const MAX_CAMERA_DIST = 40;
// 距離の初期値
const DEFFAULT_CAMERA_DIST = 30;
// 距離の計算単位
const CAMERA_DIST_CALC_UNIT = 10;
// 距離の初期値を設定
let cameraDistance = DEFFAULT_CAMERA_DIST;

// 操作用3Dオブジェクトの座標初期値
// → 表示用の3Dモデルと連動するRapier剛体(Cuboid)設定用
const OP_OBJECT_DEFAULT_POS_X = 0;
const OP_OBJECT_DEFAULT_POS_Y = 5;
const OP_OBJECT_DEFAULT_POS_Z = 2;



// 操作対象3Dオブジェクトの回りを回転するカメラの位置を計算用
let rot = 0; // 角度
let targetRot = 0;

// 物理演算を行うRapierを初期化
// Rapier物演算での重力、その重力による物理演算用の world を作成
await RAPIER.init() // This line is only needed if using the compat version
const gravity = new RAPIER.Vector3(0.0, -9.81, 0.0)
const world = new RAPIER.World(gravity)

// Three.jsによる表示のためのSceneを作成
const scene = new THREE.Scene()
scene.background = new THREE.Color( 0xbfd1e5 );
// 遠方をぼやけさせるためのfogを設定
scene.fog = new THREE.FogExp2( 0xcce0ff, 0.015 );

// 3Dモデルの操作用
let leftRotation = false
let forward = false
let backward = false
let rightRotation = false

// カメラの操作用
let camLeftRotation = false
let camRightRotation = false


// Rapierデバッグ表示用
const rapierDebugRenderer = new RapierDebugRenderer(scene, world)


// アニメーション処理用
let mixer     // glTF形式のアニメーション付きソルジャー3Dモデル用
let mixer02   // glTF形式のアニメーション付きロボット3Dモデル用
let mixer03   // glTF形式のアニメーション付きキツネ3Dモデル用


// 3Dエンジンのライト(HemisphereLight)を設定
let hemiLight = new THREE.HemisphereLight( 0xffffff, 0xffffff, 1 );
scene.add( hemiLight );

// 3Dエンジンのライト(DirectionalLight)を設定
let dirLight = new THREE.DirectionalLight( 0xffffff , 1);
scene.add( dirLight );

// 3Dエンジンのカメラを設定
const camera = new THREE.PerspectiveCamera( 30, window.innerWidth/window.innerHeight, 0.2, 5000 ); 

// 操作用3Dモデルの座標の初期値をもとに、カメラの postion.x と posision.y の座標を初期化
camera.position.x = OP_OBJECT_DEFAULT_POS_X + cameraDistance * Math.sin(rot * Math.PI / 180);
camera.position.y = DEFFAULT_CAMERA_HEIGHT; 
camera.position.z = OP_OBJECT_DEFAULT_POS_Z + cameraDistance * Math.cos(rot * Math.PI / 180);

// WebGLレンダラを作成し設定
const renderer = new THREE.WebGLRenderer({ antialias: true })
renderer.setSize(window.innerWidth, window.innerHeight)
renderer.shadowMap.enabled = true
renderer.shadowMap.type = THREE.VSMShadowMap
document.body.appendChild(renderer.domElement)

// 画面サイズ変更時にカメラ、レンダラーを再設定
window.addEventListener('resize', () => {
    camera.aspect = window.innerWidth / window.innerHeight
    camera.updateProjectionMatrix()
    renderer.setSize(window.innerWidth, window.innerHeight)
})



// 地面用の3Dモデルと物理演算用の剛体を作成

// 地面用のテクスチャを読み込み
let groundMaterial
const groundMaterialLoader = async () => {

    let loader = new THREE.TextureLoader();
    let groundTexture = loader.load( 'https://rawcdn.githack.com/mrdoob/three.js/79edf22a345079dc6cf5d8c6ad38ee22e9edab3c/examples/textures/terrain/grasslight-big.jpg' );
    groundTexture.wrapS = groundTexture.wrapT = THREE.RepeatWrapping;
    groundTexture.repeat.set( 25, 25 );
    groundTexture.anisotropy = 16;
    groundTexture.encoding = THREE.sRGBEncoding;
    groundMaterial = new THREE.MeshLambertMaterial( { map: groundTexture } );

}
await groundMaterialLoader()

// 地面の表示用3Dモデル(Box)作成
// Three.jsのBoxオブジェクトによる3Dモデルを作成し、上記で読み込んだ地面用テクスチャを設定
const floorMesh = new THREE.Mesh( new THREE.BoxGeometry( 2000, 1, 2000 ), groundMaterial )
floorMesh.position.y = -1
scene.add(floorMesh)

// 地面の表示用Box3Dモデルに連動する、Rapierによる物理演算用の剛体(Cuboid)を作成
const floorBody = world.createRigidBody(RAPIER.RigidBodyDesc.fixed().setTranslation(0, -1, 0))
const floorShape = RAPIER.ColliderDesc.cuboid( 1000, 0.5, 1000 )
world.createCollider(floorShape, floorBody)



// ソルジャーの表示用の3Dモデルと物理演算用の剛体を作成
// Three.jsのGLTFLoaderでアニメーション付きのソルジャーー3Dモデルを読み込み表示
const gltf = await new GLTFLoader().loadAsync( 'https://cdn.jsdelivr.net/gh/mrdoob/three.js@r166/examples/models/gltf/Soldier.glb' )
const gltfMesh = gltf.scene
gltfMesh.scale.set( 3, 3, 3 )

// 複数あるソルジャーー3Dモデルのアニメーションから「Walk」を設定
let animations = gltf.animations
if ( animations && animations.length ) {
    mixer = new THREE.AnimationMixer( gltf.scene );
    let animation = THREE.AnimationClip.findByName( animations, 'Walk' ) // Set animation name to play a specific animation
    mixer.clipAction( animation ).play() // Play a specific animation
}

scene.add( gltfMesh )

// 上記ソルジャー3Dモデルに連動するRapierによる物理演算用の剛体(Cuboid)を作成
//const gltfBody = world.createRigidBody(RAPIER.RigidBodyDesc.dynamic().setTranslation(0, 5, 2).setCanSleep(false))
const gltfBody = world.createRigidBody(RAPIER.RigidBodyDesc.dynamic().setTranslation(OP_OBJECT_DEFAULT_POS_X, OP_OBJECT_DEFAULT_POS_Y, OP_OBJECT_DEFAULT_POS_Z).setCanSleep(false))
gltfBody.setEnabledRotations(false, true, false, true)
const gltfShape = RAPIER.ColliderDesc.cuboid(1.5, 2.5, 1.5).setMass(1).setRestitution(0.5)
world.createCollider(gltfShape, gltfBody)



// ロボットの表示用の3Dモデルと物理演算用の剛体を作成
// Three.jsのGLTFLoaderでアニメーション付きのロボット3Dモデルを読み込み表示
const gltf02 = await new GLTFLoader().loadAsync( 'https://cdn.jsdelivr.net/gh/mrdoob/three.js@r166/examples/models/gltf/RobotExpressive/RobotExpressive.glb' )
const gltfMesh02 = gltf02.scene
gltfMesh02.scale.set( 1, 1, 1 )

// 複数あるロボット3Dモデルのアニメーションから「Idle」を設定
animations = gltf02.animations
if ( animations && animations.length ) {
    mixer02 = new THREE.AnimationMixer( gltf02.scene );
    let animation = THREE.AnimationClip.findByName( animations, 'Idle' ) // Set animation name to play a specific animation
    mixer02.clipAction( animation ).play() // Play a specific animation
}

scene.add( gltfMesh02 )

// 上記ロボット3Dモデルに連動するRapierによる物理演算用の剛体(Cuboid)を作成
const gltfBody02 = world.createRigidBody(RAPIER.RigidBodyDesc.fixed().setTranslation(5, 2.2, -10).setCanSleep(false))
const gltfShape02 = RAPIER.ColliderDesc.cuboid(1.5, 2.2, 1.5).setMass(1).setRestitution(0.5)
world.createCollider(gltfShape02, gltfBody02)



// キツネの表示用の3Dモデルと物理演算用の剛体を作成
// Three.jsのGLTFLoaderでアニメーション付きのキツネ3Dモデルを読み込み表示
const gltf03 = await new GLTFLoader().loadAsync( 'https://rawcdn.githack.com/KhronosGroup/glTF-Sample-Models/8e9a5a6ad1a2790e2333e3eb48a1ee39f9e0e31b/2.0/Fox/glTF-Binary/Fox.glb' )
//const gltfMesh03 = gltf03.scene.children[ 0 ]
const gltfMesh03 = gltf03.scene
gltfMesh03.scale.set( 0.04, 0.04, 0.04 )

// 複数あるキツネ3Dモデルのアニメーションから「Survey」を設定
animations = gltf03.animations
if ( animations && animations.length ) {
    mixer03 = new THREE.AnimationMixer( gltf03.scene );
    let animation = THREE.AnimationClip.findByName( animations, 'Survey' ) // Set animation name to play a specific animation
    mixer03.clipAction( animation ).play() // Play a specific animation
}

// 上記キツネ3Dモデルに連動するRapierによる物理演算用の剛体(Cuboid)を作成
scene.add( gltfMesh03 )
const gltfBody03 = world.createRigidBody(RAPIER.RigidBodyDesc.dynamic().setTranslation(7, 0, 4).setCanSleep(false))
gltfBody03.setEnabledRotations(false, true, false, true)
const gltfShape03 = RAPIER.ColliderDesc.cuboid(0.75, 2, 2).setMass(1).setRestitution(0.5)
world.createCollider(gltfShape03, gltfBody03)



// 赤玉の表示用の3Dモデルと物理演算用の剛体を作成
// Three.jsのSphereオブジェクトによる3Dモデルを表示
const sphereMesh = new THREE.Mesh(new THREE.SphereGeometry(), new THREE.MeshBasicMaterial({color: 0x770000}))
sphereMesh.castShadow = true
scene.add(sphereMesh)

// 上記Sphere3Dモデルに連動するRapierによる物理演算用の剛体(Ball)を作成
const sphereBody = world.createRigidBody(RAPIER.RigidBodyDesc.dynamic().setTranslation(0, 5, -3).setCanSleep(false))
const sphereShape = RAPIER.ColliderDesc.ball(1).setMass(1).setRestitution(0.5)
world.createCollider(sphereShape, sphereBody)



// 3Dモデル操作用ボタン処理
// 左回転
document.querySelector('#leftButton').onpointerdown = () => leftRotation = true
document.querySelector('#leftButton').onpointerup = () => leftRotation = false
document.querySelector('#leftButton').onpointerleave = () => leftRotation = false

// 前進
document.querySelector('#forwardButton').onpointerdown = () => forward = true
document.querySelector('#forwardButton').onpointerup = () => forward = false
document.querySelector('#forwardButton').onpointerleave = () => forward = false

// 後退
document.querySelector('#backwardButton').onpointerdown = () => backward = true
document.querySelector('#backwardButton').onpointerup = () => backward = false
document.querySelector('#backwardButton').onpointerleave = () => backward = false

// 右回転
document.querySelector('#rightButton').onpointerdown = () => rightRotation = true
document.querySelector('#rightButton').onpointerup = () => rightRotation = false
document.querySelector('#rightButton').onpointerleave = () => rightRotation = false

// カメラ操作用ボタン処理
// カメラの回転を操作
document.querySelector('#camLeftButton').onpointerdown = () => camLeftRotation = true
document.querySelector('#camLeftButton').onpointerup = () => camLeftRotation = false
document.querySelector('#camLeftButton').onpointerleave = () => camLeftRotation = false

// カメラの回転を操作
document.querySelector('#camRightButton').onpointerdown = () => camRightRotation = true
document.querySelector('#camRightButton').onpointerup = () => camRightRotation = false
document.querySelector('#camRightButton').onpointerleave = () => camRightRotation = false

// カメラの高さを操作
document.querySelector('#camHeightButton').onpointerdown = () => {
    camera.position.y = camera.position.y + CAMERA_HEIGHT_CALC_UNIT;
    if ( camera.position.y > MAX_CAMERA_HEIGHT ) {
        camera.position.y = MIM_CAMERA_HEIGHT
    }
}

// カメラの距離を操作
document.querySelector('#camDistanceButton').onpointerdown = () => {
    cameraDistance = cameraDistance - CAMERA_DIST_CALC_UNIT;
    if ( cameraDistance < MIM_CAMERA_DIST ){
        cameraDistance = MAX_CAMERA_DIST
    }
}



// キー入力・リリース処理の設定
document.addEventListener("keydown", (event) => keyDownHandler(event), false);
document.addEventListener("keyup", (event) => keyUpHandler(event), false);

// 操作用キー入力処理
const keyDownHandler = (event) => {

    // 3Dモデル操作用キー入力処理
    // 左回転
    if ( event.key === "ArrowLeft" ) {
        leftRotation = true
    // 前進
    } else if ( event.key === "ArrowUp" ) {
        forward = true
    // 後退
    } else if ( event.key === "ArrowDown" ) {
        backward = true
    // 右回転
    } else if ( event.key === "ArrowRight" ) {
        rightRotation = true

    // カメラ操作用キー入力処理
    // カメラの回転を操作
    } else if ( event.key === "a" ) {
        camLeftRotation = true
    // カメラの回転を操作
    } else if ( event.key === "d" ) {
        camRightRotation = true
    // カメラの高さを操作
    } else if ( event.key === "w" ) {
        camera.position.y = camera.position.y + CAMERA_HEIGHT_CALC_UNIT;
        if ( camera.position.y > MAX_CAMERA_HEIGHT ) {
            camera.position.y = MIM_CAMERA_HEIGHT
    }
    // カメラの距離を操作
    } else if ( event.key === "s" ) {
        cameraDistance = cameraDistance - CAMERA_DIST_CALC_UNIT;
        if ( cameraDistance < MIM_CAMERA_DIST ){
            cameraDistance = MAX_CAMERA_DIST
        }
    }

}

// 操作用キーリリース処理
const keyUpHandler = (event) => {

    // 3Dモデル操作用キーリリース処理
    // 左回転の停止
    if ( event.key === "ArrowLeft" ) {
        leftRotation = false
    // 前進の停止
    } else if ( event.key === "ArrowUp" ) {
        forward = false
    // 後退の停止
    } else if ( event.key === "ArrowDown" ) {
        backward = false
    // 右回転の停止
    } else if ( event.key === "ArrowRight" ) {
        rightRotation = false;

    // カメラ操作用キーリリース処理
    // カメラの回転を停止
    } else if ( event.key === "a" ) {
        camLeftRotation = false
    // カメラの回転を停止
    } else if ( event.key === "d" ) {
        camRightRotation = false
    }

}


// 時間設定用
const clock = new THREE.Clock()
let delta

const animate = () => {

    requestAnimationFrame(animate)

    // 物理演算のステップを進める
    delta = clock.getDelta()
    world.timestep = Math.min(delta, 0.01)
    world.step()

    // when one of buttons is pressed, apply impulse to physical cube body (cuboid)
    // state of button is changed when button down or up 

    // 操作3Dモデルと連動する剛体の左回転処理
    if ( leftRotation ) {
        gltfBody.setAngvel({ x: 0.0, y: 2.0, z: 0.0 }, true)
    }

    // 操作3Dモデルと連動する剛体の前進処理
    if ( forward ) {
        const vector = new THREE.Vector3( 0, 0, -0.1)
        vector.applyQuaternion( gltfMesh.quaternion )

         gltfBody.setTranslation({ x: gltfBody.translation().x + vector.x, y: gltfBody.translation().y + vector.y, z: gltfBody.translation().z + vector.z }, true)
    }

    // 操作3Dモデルと連動する剛体の後退処理
    if ( backward ) {
        const vector = new THREE.Vector3( 0, 0, 0.1 )
        vector.applyQuaternion( gltfMesh.quaternion )

        gltfBody.setTranslation({ x: gltfBody.translation().x + vector.x, y: gltfBody.translation().y + vector.y, z: gltfBody.translation().z + vector.z }, true)
    }

    // 操作3Dモデルと連動する剛体の後退転処理
    if ( rightRotation ) {
        gltfBody.setAngvel({ x: 0.0, y: -2.0, z: 0.0 }, true)
    }


    // カメラ操作の計算・設定その1 START 

    // カメラ左回転ボタン押下時の計算処理
    if(camLeftRotation) {
        targetRot = targetRot - 2
    // カメラ右回転ボタン押下時の計算処理
    } else if ( camRightRotation ) {
        targetRot = targetRot + 2
    }

    //rot += 0.5; // 毎フレーム角度を0.5度ずつ足していく
    // イージングの公式を用いて滑らかにする
    // 値 += (目標値 - 現在の値) * 減速値
    rot += (targetRot - rot) * 0.05

    // カメラ操作の計算・設定その1 END 


    // 物理演算した剛体の座標と回転を連動する表示用3Dモデルに反映 START 

    // ソルジャー3Dモデルの設定
    gltfMesh.position.copy(gltfBody.translation())
    gltfMesh.quaternion.copy(gltfBody.rotation())
    // glTFオブジェクトの座標と回転を表示用に調整
    gltfMesh.position.y = gltfMesh.position.y - 2.5

    // ロボット3Dモデルの設定
    gltfMesh02.position.copy(gltfBody02.translation())
    gltfMesh02.quaternion.copy(gltfBody02.rotation())
    // glTFオブジェクトの座標と回転を表示用に調整
    gltfMesh02.position.y = gltfMesh02.position.y - 2.2

    // キツネ3Dモデルの設定
    gltfMesh03.position.copy(gltfBody03.translation())
    gltfMesh03.quaternion.copy(gltfBody03.rotation())
    // glTFオブジェクトの座標と回転を表示用に調整
    gltfMesh03.position.y = gltfMesh03.position.y - 2
    gltfMesh03.rotation.y = gltfMesh03.rotation.y + Math.PI

    // 赤球の設定
    sphereMesh.position.copy(sphereBody.translation())
    sphereMesh.quaternion.copy(sphereBody.rotation())

    // 物理演算した剛体の座標と回転を連動する表示用3Dモデルに反映 END 


    // デバッグの描画を更新
    rapierDebugRenderer.update()


    // カメラ操作の計算・設定その2 START

    // 角度に応じてカメラの位置を設定
    camera.position.x = gltfMesh.position.x + cameraDistance * Math.sin(rot * Math.PI / 180)
    camera.position.z = gltfMesh.position.z + cameraDistance * Math.cos(rot * Math.PI / 180)
    // 操作対象3Dオブジェクトを見つめる
    camera.lookAt(new THREE.Vector3( gltfMesh.position.x, gltfMesh.position.y, gltfMesh.position.z ))

    // カメラ操作の計算・設定その2 END


    // 3Dモデルのアニメーションを更新
    if (mixer) mixer.update(delta)
    if (mixer02) mixer02.update(delta)
    if (mixer03) mixer03.update(delta)


    renderer.render(scene, camera)

}

animate()


Practice 32_03 Omake

Three.js 0.166.0 & Rapier 0.13.1 Practice 32_03 Omake

HTML 省略(他と同じ)

CSS 省略(他と同じ)

JavaScript 省略

 Practice 32_02でのsetTranslationの設定値の値を増やすだけです。 さらに増やすとワープ状態となりますw。 さらにワープ状態では物理対象ともほとんどぶつからず、物理演算の意味ねぇぇぇ~、状態となるようでしたw。


一時まとめ


いいなと思ったら応援しよう!