当前位置: 首页 > 工具软件 > gravity.js > 使用案例 >

THREE.js知识点整理

岳飞航
2023-12-01

three.js必备元素创建

thress.js要显示一个3D模型的必备元素:scene,camera.light,renderer

H5写法:

 <script src="https://cdn.bootcdn.net/ajax/libs/three.js/r127/three.min.js"></script>
 var scene = new THREE.Scene();
 var camera = new THREE.PerspectiveCamera(75,window.innerWidth/window.innerHeight,0.1,1000);
 camera.position.z = 5;
 var aLight = new THREE.AmbientLight(0xffffff,1);
 scene.add(aLight);
 var renderer = new THREE.WebGLRenderer({ });
 renderer.setSize(window.innerWidth,window.innerHeight);
 document.body.appendChild(renderer.domElement);
  renderer.render(scene,camera);

微信小程序写法:

<canvas id="webgl" canvas-id="webgl" class="webgl" type="webgl" disable-scroll="{{true}}"></canvas>
微信小程序的107版本写法
const { createScopedThreejs } = require("threejs-miniprogram");
var THREE, renderer, scene, camera, canvas;
function initThree(canvasId, callback) {
    const query = wx.createSelectorQuery();
    query
        .select("#" + canvasId)
        .node()
        .exec((res) => {
            canvas = res[0].node;
            THREE = createScopedThreejs(canvas);
            THREE.canvas = canvas;
            initScene();
            if (typeof callback === "function") {
                callback(THREE);
            }
        });
}

function initScene() {
    scene = new THREE.Scene();
    scene.add(new THREE.AmbientLight(0xffffff, 1.5));
  
    camera = new THREE.PerspectiveCamera( 75, canvas.width / canvas.height, 1, 1000  );
    camera.position.set(0, 3, 5);

    renderer = new THREE.WebGLRenderer({   antialias: true, alpha: true,  preserveDrawingBuffer: true, });
    
    const systemInfo = wx.getSystemInfoSync();
    if (systemInfo.system.indexOf("Android") !== -1) {
        renderer.setPixelRatio(systemInfo.pixelRatio); // 安卓适配像素比
        renderer.setSize(canvas.width, canvas.height);
    } else {
        const ratio = systemInfo.pixelRatio; // ios 适配像素比展示
        const width = systemInfo.windowWidth;
        const height = systemInfo.windowHeight;
        let canvasWidth = width * ratio;
        let canvasHeight = height * ratio;
        renderer.setSize(canvasWidth, canvasHeight);
    }

    moveAnimate();
}
function moveAnimate() {
    animateId = canvas.requestAnimationFrame(moveAnimate);
    renderer.render(scene, camera);
}
微信小程序127版本写法
const { THREE, WechatPlatform, GLTFLoader, OrbitControls } = require("threejs-127/index-127.js");
var renderer, scene, camera, canvas, platform, disposing = false, frameId, controls;
function initThree(canvasId, callback) {
    const query = wx.createSelectorQuery();
    query
        .select("#" + canvasId)
        .node()
        .exec((res) => {
            canvas = res[0].node;
            platform = new WechatPlatform(canvas);
            THREE.PLATFORM.set(platform);
            initScene();
            if (typeof callback === "function") {
                callback(THREE, gltfLoader);
            }
        });
}

function initScene() {
    scene = new THREE.Scene();
    scene.add(new THREE.AmbientLight(0xffffff, 0.5));
    
    camera = new THREE.PerspectiveCamera(75, canvas.width / canvas.height, 0.0001, 1000);
    camera.position.set(0, 3, 5);

    renderer = new THREE.WebGL1Renderer({ canvas, antialias: true, alpha: true });
    renderer.outputEncoding = THREE.sRGBEncoding
    renderer.setSize(canvas.width, canvas.height)
    renderer.setPixelRatio(THREE.$window.devicePixelRatio);
    disposing = false;
    const render = () => {
      if (!disposing) frameId = THREE.$requestAnimationFrame(render);
      renderer.render(scene, camera);
  }
  render()
}

3D模型加载方式

外部加载gltf等格式的模型:

<script src="js/loaders/GLTFLoader.js"></script>
 const gltfLoader = new GLTFLoader();
 gltfLoader.load(modelUrl,function(gltf){
           const model = gltf.scene;
           model.position.set(0,-30,150);
           model.scale.set(300,300,300);
           model.rotation.set(0.3,0,0);
           scene.add(model);
       });

自己创建:

基本要素: 网格形状Geometry ,材质material,纹理texture(贴在材质上的图片,可省略),网格Mesh

var boxGeo = new THREE.BoxGeometry(1,1,1);
   var sphereGeo = new THREE.SphereGeometry(0.5,40,40);
   var cylinderGeo = new THREE.CylinderGeometry(0.5,0.8,1,25);
   var octGeo = new THREE.PlaneGeometry(1,1);

var textureLoader = new THREE.TextureLoader();
   textureLoader.load('./imgs/1.jpg',function(texture){// 异步,
       var mat = new THREE.MeshLambertMaterial({map:texture,side: THREE.DoubleSide});

       var box = new THREE.Mesh(boxGeo,mat);
       scene.add(box);

       var sphere = new THREE.Mesh(sphereGeo,mat);
       sphere.position.set(2,0,0);
       scene.add(sphere);

       var cylinder = new THREE.Mesh(cylinderGeo,mat);
       cylinder.position.set(-2,0,0);
       scene.add(cylinder);

       var oct = new THREE.Mesh(octGeo,mat);
       oct.position.set(-2,-2,0);
       scene.add(oct)
   });
   
// 纹理对象与图片加载器
   var imageLoader = new THREE.ImageLoader();
   imageLoader.load('./imgs/2.jpg',function(img){
       var texture = new THREE.Texture(img);// texture.image的属性值就是一张图片
       texture.needsUpdate = true;
       var mat = new THREE.MeshLambertMaterial({map:texture});
       var mesh = new THREE.Mesh(boxGeo,mat);
       mesh.position.y = 2;
       scene.add(mesh);
   })

视频纹理

H5的实现方式

#coverVideo{
    /* visibility: hidden; */
    position: fixed;
    /* left: 0px; */
    left: -1000000px;
    top:0px;
    z-index: 10000;
    width: 320px;
    height: 240px;
    background-color: pink;
  }
<video id="coverVideo" loop controls  webkit-playsinline="true" x-webkit-airplay="true"
    playsinline="true" x5-video-player-type="h5" preload="auto"></video>
<canvas id="canvas"></canvas>
  const coverVideoTarget = document.getElementById('coverVideo');
  function setVideo(videoUrl){
      coverVideoTarget.src = videoUrl;
      coverVideoTarget.play();
      
      const texture = new THREE.VideoTexture(coverVideoTarget);
      var mat = new THREE.MeshBasicMaterial({map: texture,transparent:true});
      const geo = new THREE.PlaneGeometry(400,240);
      const coverVideo = new THREE.Mesh(geo, mat);
      scene.add(coverVideo);
      // 可使用coverVideoTarget.pause()暂停视频纹理的播放;
  }

使用canvas纹理来实现视频纹理

const coverVideoTarget = document.getElementById('coverVideo');
function setVideo2(videoUrl){
      coverVideoTarget.src = videoUrl;
      let vWidth = 400, vHeight = 240;
      coverVideoTarget.addEventListener('canplay', function () {
        vWidth = this.videoWidth;
        vHeight = this.videoHeight;
      });

      var canvas_process = document.createElement('canvas');
      var context_process = canvas_process.getContext('2d');
      const texture = new THREE.CanvasTexture(canvas_process);
      function update() {
        context_process.fillStyle = 'black';
        context_process.fillRect(0, 0, vWidth, vHeight);
        context_process.drawImage(coverVideoTarget, 0, 0, vWidth, vHeight);
        texture.needsUpdate = true;
        requestAnimationFrame(update);
      }
      update();
      
      var mat = new THREE.MeshBasicMaterial({map: texture,transparent:true});
      const geo = new THREE.PlaneGeometry(400,240);
      coverVideo = new THREE.Mesh(geo, mat);
      scene.add(coverVideo);
  }

微信小程序中实现视频纹理:(使用Threejs 的分块序列图播放工具)

<canvas type="webgl" id="canvas"></canvas>
import ThreeSpritePlayer from '../../tsp';
const { THREE, WechatPlatform, GLTFLoader, OrbitControls } = require("threejs-miniprogram/index-127.js");

const tile = {
  url: Array(3)
    .fill(0)
    .map((v, k) => `/imgs/output-${k}.png`),
  col: 2,
  row: 2,
  total: 10,
  fps: 16,
};
let disposing,frameId,platform;

function getNode(id, ctx, fields = { node: true, rect: true, size: true }) {
  return new Promise(function (resolve) {
    wx.createSelectorQuery().in(ctx).select(id).fields(fields).exec(resolve);
  });
}

Page({
  async onLoad() {
    const canvas = (await getNode('#canvas', this))[0].node;
    platform = new WechatPlatform(canvas);
    THREE.PLATFORM.set(platform);

    const scene = new THREE.Scene();
    const camera = new THREE.PerspectiveCamera( 75,  canvas.width / canvas.height,  0.1, 100  );
    const textureLoader = new THREE.TextureLoader();
    
    Promise.all(
      tile.url.map(
        url =>
          new Promise((resolve, reject) => {
            textureLoader.load(
              url,
              texture => resolve(texture),
              undefined,
              reject,
            );
          }),
      ),
    ).then(textures => {
      const spritePlayer = new ThreeSpritePlayer(  textures, tile.total,  tile.row,  tile.col, tile.fps, false, );
      // 可以自行构建mesh
      const material = new THREE.MeshBasicMaterial({
        map: spritePlayer.texture,
        transparent: false,
      });

      const boxGeo = new THREE.BoxGeometry(5,5,5);
      const boxMesh = new THREE.Mesh(boxGeo, material);
      boxMesh.position.set(0,0,-15);
      boxMesh.rotation.set(0,0.5,0);
      scene.add(boxMesh);
      // spritePlayer.stop();

      var renderer = new THREE.WebGL1Renderer({ canvas, antialias: true, alpha: true });
      renderer.outputEncoding = THREE.sRGBEncoding
      renderer.setSize(canvas.width, canvas.height)
      renderer.setPixelRatio(THREE.$window.devicePixelRatio);
      disposing = false;
      const render = () => {
        if (!disposing) frameId = THREE.$requestAnimationFrame(render);
        spritePlayer.animate();
        // 更新material.map
        material.map = spritePlayer.texture;
        renderer.render(scene, camera);
      }
      render()
    });
  }
});

透明纹理

var material = new THREE.MeshBasicMaterial(
        {
            map: textureLoader.load(texure_url),
            opacity:0.99,
            transparent: true
        }
    );//材料

模型纹理闪烁的问题

设置模型的材质material.depthTest = false;

Sprite

// data.json
[{"name":"seat_F2_01","state":"canReserve","position":{"x":0.343436778,"y":-0.010585973,"z":-0.156569347}},
{"name":"seat_F2_02","state":"canReserve","position":{"x":0.355292529,"y":-0.010585974,"z":-0.138982728}}]
var canTexture = new THREE.TextureLoader().load("./imgs/can.jpg");
var cannotTexture = new THREE.TextureLoader().load("./imgs/cannot.jpg");
let group = new THREE.Group();
var loader = new THREE.FileLoader().setResponseType('json');
gltfLoader.load(modelUrl,function(gltf){
    const model = gltf.scene;
    model.rotation.set(0.3,0,0);
    scene.add(model);
    loader.load('./js/data.json', function(data) {
        data.forEach(elem => {
            let texture = elem.state === 'canReserve' ? canTexture : cannotTexture;
            var spriteMaterial = new THREE.SpriteMaterial({
                map: texture, //设置精灵纹理贴图
                transparent: true,
                opacity: 0.5,
            });
            var sprite = new THREE.Sprite(spriteMaterial);
            group.add(sprite);
            sprite.scale.set(0.5, 0.5, 1);
            sprite.position.set(elem.position.x, elem.position.y, elem.position.z)
        });
        group.position.set(0, 0.01, 0);
        model.add(group);//把精灵群组插入场景中
    })

    // 用于遍历模型得到椅子的位置(只在开发阶段使用)
    // var seatJson = [];
    // model.traverse(item => {
    //     if (item.name.indexOf('seat') !== -1) {
    //         seatJson.push({ name: item.name, state: 'canReserve', position: item.position});
    //     }
    // })
    // console.log(JSON.stringify(seatJson));
    
});

box给不同面赋予不同的贴图

// 创建一个明信片,明信片正反面贴图不一样
var box = new THREE.BoxGeometry(20,15,0.2);

var textureLoader = new THREE.TextureLoader();
let mat = makeMat(`https://houtaicdn.alva.com.cn/medias/resources/wechat/arread/chongqing/mingxinpian/card-scene1.jpg`);
var textMat = makeMat(`https://houtaicdn.alva.com.cn/medias/resources/wechat/arread/chongqing/mingxinpian/card-demo1.png`)

let card = makeCard();
scene.add(card)

function makeMat(imgUrl) {
    var texture = textureLoader.load(imgUrl);
    texture.minFilter = THREE.LinearFilter;
    var mat = new THREE.MeshBasicMaterial({map: texture});
    return mat;
}
      
function makeCard() {
    var matArr = [mat,textMat];
    box.faces[10].materialIndex = 1;
    box.faces[11].materialIndex = 1;
    box.faces[8].materialIndex = 0;
    box.faces[9].materialIndex = 0;
	// 如果box没有faces属性,看有没有faceVertexUvs属性,这两个属性均是用于设置哪个面对应哪张纹理的
	
    var card = new THREE.Mesh(box, matArr);
    return card;
  }

3D动物模型在某一空间范围内随机自由移动

function hudieTrans(model){
    if (!model) return;
    if(model.targetPosition === undefined){
        model.targetPosition = new THREE.Vector3(model.position.x, model.position.y, model.position.z);
    }
    if (model.position.distanceTo(model.targetPosition) < 0.4)  {
        var targetPosition = model.targetPosition = new THREE.Vector3(randomNum(-50, 50), randomNum(-5, 5), randomNum(-50, 50));
        var dirV3 = new THREE.Vector3();
        dirV3.subVectors(model.targetPosition, model.position);
        model.speedV3 = dirV3.normalize();
        model.speedV3 = v3Scale(model.speedV3, 0.02);
        model.lookAt( targetPosition.x, targetPosition.y, targetPosition.z);// 改变模型的方向
    } else  {
        model.position.x += model.speedV3.x;
        model.position.y += model.speedV3.y;
        model.position.z += model.speedV3.z;
    }
}

function v3Scale(v3,scale){
    return new THREE.Vector3(v3.x * scale, v3.y * scale, v3.z * scale)
}

function randomNum(minNum, maxNum) {
    switch (arguments.length) {
        case 1:
            return parseInt(Math.random() * minNum + 1, 10);
        case 2:
            return parseInt(Math.random() * (maxNum - minNum + 1) + minNum, 10);
        default:
            return 0;
    }
}

模型的缩放移动旋转(单指旋转,双指移动、缩放)

方法一:使用插件

H5写法
<script src="js/OrbitControls.js"></script>
var controls = new THREE.OrbitControls(camera)
controls.enableZoom = true
 //controls.autoRotate = true;
 controls.minDistance = 10;
 controls.maxDistance = 300;
 controls.maxPolarAngle = 1.5;
 controls.minPolarAngle = 1.5;
 controls.enablePan = false;
 animate()
 function animate(){
        controls.update()
        requestAnimationFrame(animate);
        renderer.render(scene,camera);
    }

微信小程序(版本127)

<canvas  id="webgl" canvas-id="webgl" class="webgl" type="webgl" disable-scroll="{{true}}"  bindtouchstart="onTX" bindtouchmove="onTX" bindtouchend="onTX"></canvas>
const { THREE, WechatPlatform, GLTFLoader, OrbitControls } = require("threejs-127/index-127.js");

controls = new OrbitControls(camera, canvas);
controls.enableDamping = true

const render = () => {
      if (!disposing) frameId = THREE.$requestAnimationFrame(render);
      controls.update();
      renderer.render(scene, camera);
  }
 render()
 
onTX(e) {
  platform.dispatchTouchEvent(e)
}

方法二:自己手写(应用于微信小程序中,有兴趣的可以根据这些适配到H5)

<canvas  id="webgl" canvas-id="webgl" class="webgl" type="webgl" disable-scroll="{{true}}" bindtouchmove='ontouchmove' bindtouchend="ontouchend"></canvas>
let first = true,
    isTwoTouch = false,
    twoFirst = true,
    lastPositon = {},
    lastDistance,
    lastPoint = {},
    steps = 1;
ontouchmove(e) {
    let touches = e.touches;
    if (touches.length === 1) {
        if (isTwoTouch) {
            return;
        }
        if (first) {
            lastPositon.x = touches[0].x;
            lastPositon.y = touches[0].y;
            first = false;
        } else {
            let deltaY = (touches[0].y - lastPositon.y) / 100;
            let deltaX = (touches[0].x - lastPositon.x) / 100;
            lastPositon.x = touches[0].x;
            lastPositon.y = touches[0].y;
            currModel.rotation.x += deltaY;
            currModel.rotation.y += deltaX;
        }
    } else if (touches.length === 2) {
        isTwoTouch = true;
        let t1 = touches[0],
            t2 = touches[1];
        if (twoFirst) {
            lastDistance = this.calDistance(t1, t2);
            lastPoint = t1;
            twoFirst = false;
        } else {
            let currDistance = this.calDistance(t1, t2);
            let factor = 0.001 * (currDistance - lastDistance);
            if (Math.abs(factor) < 0.001) {
                // 移动
                var disX = (touches[0].x - lastPoint.x) * 0.01;
                var disY = -(touches[0].y - lastPoint.y) * 0.01;
                currModel.position.x += disX;
                currModel.position.y += disY;
            } else {
                currModel.scale.x += factor;
                currModel.scale.y += factor;
                currModel.scale.z += factor;
            }
            lastDistance = currDistance;
            lastPoint.x = touches[0].x;
            lastPoint.y = touches[0].y;
        }
    }
},
calDistance(t1, t2) {
    let v1 = new THREE.Vector2(t1.x, t1.y);
    let v2 = new THREE.Vector2(t2.x, t2.y);
    return v1.distanceTo(v2);
},
ontouchend() {
    first = true;
    twoFirst = true;
    lastPositon.x = 0;
    lastPositon.y = 0;
    lastPoint.x = 0;
    lastPoint.y = 0;
    isTwoTouch = false;
},

检测3D模型的缩放位置旋转方向是否符合规定值

checkMatch() {
        if (!currModel) return;
        let { position, rotation, scale } = currModel;// 获取当前模型的当前状态值
        let { cPosition, cRotation, cScale } = config[mIndex];// 获取规定数值
        let p = new THREE.Vector3(position.x, position.y, position.z);
        let cP = new THREE.Vector3(cPosition.x, cPosition.y, cPosition.z);
        let r = new THREE.Vector3(rotation.x, rotation.y, rotation.z);
        let cR = new THREE.Vector3(cRotation.x, cRotation.y, cRotation.z);
        // console.log(p.distanceTo(cP).toFixed(2), r.angleTo(cR).toFixed(2), Math.abs(scale.x - cScale).toFixed(2));
        // console.log((r.angleTo(cR)).toFixed(2))
        // console.log(position, rotation, scale, "------");
        if (p.distanceTo(cP) > 0.45) {// 0.45这个值为允许误差值
            return "position not match";
        }
        if (r.angleTo(cR) > 0.45) {// 0.45这个值为允许误差值
            return "angle not match";
        }
        if (Math.abs(scale.x - cScale) > 0.45) {// 0.45这个值为允许误差值
            return "scale not match";
        }
        return "match";
    },

游戏摇杆的开发

H5写法

 // 绘制摇杆
function initRocker(){
     let outerDiameter = 100;// 外圆直径
     let innerDiameter = 35;// 内圆直径
     let outerRadius = outerDiameter / 2;
     let innerRadius = innerDiameter / 2;
     let centerNum = (outerDiameter - innerDiameter) / 2;// 内圆位置
     let rockerBox = document.createElement('div');
     setStyle(rockerBox,{
         width: `${outerDiameter}px`,
         height: `${outerDiameter}px`,
         borderRadius: `${outerRadius}px`,
         position: 'fixed',
         bottom: '2rem',
         right: '4rem',
         zIndex: 100,
         background: 'url("./imgs/rocker-bg.png") no-repeat center',
         backgroundSize: 'contain'
     });
     document.body.appendChild(rockerBox);

     let rockerBtn = document.createElement('div');
     setStyle(rockerBtn,{
         position: 'absolute',
         width: `${innerDiameter}px`,
         height: `${innerDiameter}px`,
         left: `${centerNum}px`,
         bottom: `${centerNum}px`,
         borderRadius: `${innerRadius}px`,
         background: '#fbbb1d',
     });
     rockerBox.appendChild(rockerBtn);

     // 添加移动监控事件
     let startPos = {x:0,y:0};
     let disX = 0,disY = 0;

     function onDown(e){
         startPos.x = e.clientX || e.touches[0].clientX;
         startPos.y = e.clientY || e.touches[0].clientY;
         document.addEventListener('mousemove',onMove,false);
         document.addEventListener('touchmove',onMove,false);
     }
     function onMove(e){
         let clientX = e.clientX || e.touches[0].clientX;
         let clientY = e.clientY || e.touches[0].clientY;
         let maxNum = centerNum + 5;
         disX = (clientX - startPos.x) ;
         disY = (clientY - startPos.y) ;
         // 圆心位置 (100,100) (div.style.x + 40, div.style.y + 40)
         disX = disX > maxNum ? maxNum : (disX < -maxNum ? -maxNum : disX);
         disY = disY > maxNum ? maxNum : (disY < -maxNum ? -maxNum : disY);
         if ((Math.pow(disX,2) + Math.pow(disY,2)) > Math.pow(maxNum,2)) {
             if (disY > 0) {
                 disY = Math.sqrt(Math.pow(maxNum, 2) - Math.pow(disX,2));
             } else if (disY < 0) {
                 disY = -Math.sqrt(Math.pow(maxNum, 2) - Math.pow(disX,2));
             }
         }
         
         rockerBtn.style.transform = `translate(${disX}px,${disY}px)`;
     }
     rockerUp = function (e){
         document.removeEventListener('mousemove',onMove,false);
         document.removeEventListener('touchmove',onMove,false);
         disX = 0;
         disY = 0;
         rockerBtn.style.transform = 'translate(0,0)';
     }

     rockerBtn.addEventListener('mousedown',onDown,false);
     document.body.addEventListener('mouseup',rockerUp,false);

     rockerBtn.addEventListener('touchstart',onDown,false);
     document.body.addEventListener('touchend',rockerUp,false);

     function moveFrame(){
         requestAnimationFrame(moveFrame);
         if (Math.abs(disX) > Math.abs(disY)) {
             if (disX > 0) {
                 // up [0,1]  down [0,-1]  left [-1,0] right [1,0]
                 dirImpulse = [1,0];// 设置行走方向
             } else if (disX < 0) {
                 dirImpulse = [-1,0];
             }
         } else {
             if (disY > 0) {
                 dirImpulse = [0,-1];
             } else if (disY < 0) {
                 dirImpulse = [0,1] ;
             }
         }
     }
     moveFrame();
 };
// 工具函数         
function setStyle(dom,options,fn){
    new Promise(function(resolve,reject){
        for (let key in options){
            dom.style[key] = options[key];
        }
        resolve();
    }).then(res => {
        if (fn) {
            fn()
        }
    }).catch(err => {
        console.log(err)
    })
}  

微信小程序摇杆

<view class="rocker-box" bindtouchstart="startFn" bindtouchmove="moveFn" bindtouchend="endFn">
   <view class="rocker-inner" style="{{transform}}" ></view>
   <image src='https://houtaicdn.alva.com.cn/medias/resources/wechat/arread/maze5/imgs/rocker-bg3.png' mode="widthFix"></image>
</view>
.rocker-box {
  position: fixed;
  right: 20rpx;
  bottom: 20rpx;
  z-index: 100;
  width: 100rpx;
  height: 100rpx;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  /* background: url("https://houtaicdn.alva.com.cn/medias/resources/wechat/arread/maze5/imgs/rocker-bg.png") no-repeat center;
  background-size: 'contain'; */
}

.rocker-box image {
  position: absolute;
  left: 0;
  top: 0;
  width: 100rpx;
  height: 100rpx;
}

.rocker-inner {
  width: 40rpx;
  height: 40rpx;
  /* background-color: #fbbb1d; */
  
  background: url("https://houtaicdn.alva.com.cn/medias/resources/wechat/arread/maze5/imgs/rocker-i.png") no-repeat center;
  background-size: 100% 100%;
  border-radius: 50%;
  z-index: 100;
}
const RADIUS = 27
const MIN_DISTANCE = 1.5 //至少位移到5才触发摇杆

// rocker相关
 startPoint: "",
 startFn(e) {
     let touch = e.touches[0];
     this.startPoint = [touch.pageX, touch.pageY];
 },
 moveFn(e) {
     let touch = e.touches[0];
     let point = [touch.pageX, touch.pageY];
     let differ = [point[0] - this.startPoint[0], point[1] - this.startPoint[1]];
     let distance = Math.sqrt(differ[0] * differ[0] + differ[1] * differ[1]);
     if (distance > MIN_DISTANCE) {
         // 摇杆始终切到边缘,实际位移距离无关紧要,只关心移动方向
         let rate = RADIUS / distance;
         let position = [differ[0] * rate, differ[1] * rate];
         this.setData({
             transform: "transform: translate(" + position[0] + "px, " + position[1] + "px)"
         });
         if (Math.abs(position[0]) > Math.abs(position[1])) {
             if (position[0] < 0) {
                 threeBusiness.setDirImpulse(-1, 0);// 设置行走方向
             } else if (position[0] > 0) {
                 threeBusiness.setDirImpulse(1, 0);
             }
         } else {
             if (position[1] < 0) {
                 threeBusiness.setDirImpulse(0, 1);
             } else if (position[1] > 0) {
                 threeBusiness.setDirImpulse(0, -1);
             }
         }
     }
 },
 endFn(e) {
     this.setData({
         transform: ""
     });
 },

物理引擎cannon.js 固定约束/关节约束/辅助线/

three.js没有自己的物理引擎,需要引入外部物理引擎来实现物理效果
渲染引擎和物理引擎结合来实现碰撞检测、自由落体等物理现象,其核心实质就是将物理引擎的计算结果更新到渲染引擎中。
(待更新…)

使用陀螺仪与3D模型构建AR场景

H5中:引入 DeviceOrientationControls

import { DeviceOrientationControls } from './jsm/controls/DeviceOrientationControls.js';
deviceControl = new DeviceOrientationControls(camera);
function animate() {
	window.requestAnimationFrame( animate );
	deviceControl.update();
	renderer.render( scene, camera );
}

微信小程序中

const deviceOrientationControl = require("./DeviceOrientationControl.js");
var deviceMotion = false; // 是否使用陀螺仪保持原位
var lastDevice = {},
    device = {};
function moveAnimate() {
    animateId = canvas.requestAnimationFrame(moveAnimate);
    if (
        lastDevice.alpha !== device.alpha ||
        lastDevice.beta !== device.beta ||
        lastDevice.gamma !== device.gamma
    ) {
        // 手机的方位发生了移动
        lastDevice.alpha = device.alpha;
        lastDevice.beta = device.beta;
        lastDevice.gamma = device.gamma;
        if (deviceMotion) {
            // 操作模型使保持在空间中的原位
            deviceOrientationControl.deviceControl(
                camera,
                device,
                THREE,
                isAndroid
            );
        }
    }
    renderer.render(scene, camera);
}

function startDeviceMotion() {
    deviceMotion = true;
    wx.onDeviceMotionChange(function (_device) {
        device = _device;
    });
    wx.startDeviceMotionListening({
        interval: "ui",
        success: function () {
            console.log("startDeviceMotionListening", "success");
        },
        fail: function (error) {
            console.log("startDeviceMotionListening", error);
        },
    });
}
function stopDeviceMotion() {
    deviceMotion = false;
    wx.offDeviceMotionChange();
    wx.stopDeviceMotionListening({
        success: function () {
            console.log("stopDeviceMotionListening", "success");
        },
        fail: function (error) {
            console.log("stopDeviceMotionListening", error);
        },
    });
}

微信小程序中实现AR拍照保存照片

const { screenshot } = require("./screenshot");

takePhoto() {
    let self = this;
    wx.showLoading({
        title: "图片处理中...",
    });
    this.data.cameraContext.takePhoto({
        quality: "low",
        success: res => {
            self.webglToPhoto(res.tempImagePath);
        },
    });
},
webglToPhoto(photo) {
    let self = this;
    const query = wx.createSelectorQuery();
    query
        .select("#canvas")
        .fields({ node: true, size: true })
        .exec(res => {
            self.clipWebgl(res[0].node, photo).then(webglImg => {
                const ctx = wx.createCanvasContext("photo");
                const { width, height, platform } = self.data.sysInfo;
                ctx.drawImage(photo, 0, 0, width, height);
                ctx.drawImage(webglImg.path, 0, 0, width, height);
                ctx.draw(true);
                 self.canvasToImg(ctx, width, height);// canvas已经绘制成功了,但小程序中canvas元素不能缩放展示,要想缩放展示尺寸,就要转成img再展示img
            });
        });
    return;
},
clipWebgl(helperCanvas,photo){
 	return new Promise((resolve,reject) => {
     const [data, w, h] = screenshot(renderer, scene, camera, THREE.WebGLRenderTarget);
     // resolve(data.buffer);
     const ctx = helperCanvas.getContext('2d');
     const imgData = helperCanvas.createImageData(data, w, h);
     helperCanvas.height = imgData.height;
     helperCanvas.width = imgData.width;
     ctx.putImageData(imgData, 0, 0);
     wx.canvasToTempFilePath({
         canvas: helperCanvas,
         success(res) {
             resolve({path: res.tempFilePath, width: imgData.width, height: imgData.height})
         },
         fail(err){
             reject(err);
         }
     })
 })
},
canvasToImg(ctx, width, height) {
    let self = this;
    ctx.draw(
        true,
        setTimeout(() => {
            wx.canvasToTempFilePath({
                quality: 0.5,
                x: 0,
                y: 0,
                width: width,
                height: height,
                destWidth: width * wx.getSystemInfoSync().pixelRatio,
                destHeight: height * wx.getSystemInfoSync().pixelRatio,
                canvasId: "photo",
                fileType: "jpg",
                success(res) {
                    console.log("photo绘制成功");
                    wx.hideLoading();
                    self.setData({
                        photoUrl: res.tempFilePath
                    });
                }
            });
        }, 300)
    );
},
function flip(pixels, w, h, c) {
  // handle Arrays
  if (Array.isArray(pixels)) {
    var result = flip(new Float64Array(pixels), w, h, c);
    for (var i = 0; i < pixels.length; i++) {
      pixels[i] = result[i];
    }
    return pixels;
  }

  if (!w || !h) throw Error('Bad dimensions');
  if (!c) c = pixels.length / (w * h);

  var h2 = h >> 1;
  var row = w * c;
  var Ctor = pixels.constructor;

  // make a temp buffer to hold one row
  var temp = new Ctor(w * c);
  for (var y = 0; y < h2; ++y) {
    var topOffset = y * row;
    var bottomOffset = (h - y - 1) * row;

    // make copy of a row on the top half
    temp.set(pixels.subarray(topOffset, topOffset + row));

    // copy a row from the bottom half to the top
    pixels.copyWithin(topOffset, bottomOffset, bottomOffset + row);

    // copy the copy of the top half row to the bottom half
    pixels.set(temp, bottomOffset);
  }

  return pixels;
};
function screenshot(renderer, scene, camera, WebGLRenderTarget) {
  // const { width, height } = renderer.domElement;
  const width = 720,height = 1280;
  const renderTarget = new WebGLRenderTarget(width, height);
  const buffer = new Uint8Array(width * height * 4);

  renderTarget.texture.encoding = renderer.outputEncoding;
  renderer.setRenderTarget(renderTarget);
  renderer.render(scene, camera);
  renderer.readRenderTargetPixels(renderTarget, 0, 0, width, height, buffer);
  renderer.setRenderTarget(null);
  renderTarget.dispose();

  flip(buffer, width, height, 4);
  return [buffer, width, height];
}
export {screenshot}

Layabox 以人物第一视角在3D空间(模型)中自由操控移动转向(不穿越物体)

  • 3D空间模型中含有静态碰撞器 PhysicsCollider

import BigMoveScript from './script/BigMoveScript';

addRigidbody(){
	let mat2 = new Laya.BlinnPhongMaterial();
    Laya.Texture2D.load("res/plywood.jpg", Laya.Handler.create(this, function (tex) {
    		 mat2.albedoTexture = tex;
     }));
    var raidius =  0.5;
	var height = 1;
    this.rigidMesh = this.scene.addChild(new Laya.MeshSprite3D(Laya.PrimitiveMesh.createCapsule(raidius, height)));// 创建角色碰撞器
    this.rigidMesh.meshRenderer.material = this.mat2;
	let cPos = camera.transform.position;
	this.rigidMesh.transform.position = new Laya.Vector3(cPos.x,1,cPos.z);
	this.rigidMesh.transform.rotation = new Laya.Vector3(0,-1.8,0);
	this.rigidMesh.addComponent(BigMoveScript);

	let rigidBody = this.rigidMesh.addComponent(Laya.CharacterController);
	// rigidBody.upAxis = new Laya.Vector3(0,1,0);
    // let rigidBody = this.rigidMesh.addComponent(Laya.Rigidbody3D);
    let sphereShape = new Laya.CapsuleColliderShape(raidius, height);
    rigidBody.colliderShape = sphereShape;
    rigidBody.mass = 1;
	rigidBody.gravity = new Laya.Vector3(0,0,0);
	// this.rockerView = new JoyStick(this.rigidMesh);	
}
animate(){
	var _this = this;
	if (this.rigidMesh) {
		camera.transform.rotation = this.rigidMesh.transform.rotation;
		camera.transform.position = this.rigidMesh.transform.position;
	}
	requestAnimationFrame(_this.animate.bind(_this));
}
export default class BigMoveScript extends Laya.Script3D{// 操作方法:点击开始行走,再点击暂停,在屏幕上移动手指旋转;
	constructor(){
		super();
		this.scene = null;
		this.lastPosition = new Laya.Vector2(0, 0);
		this.distance = 0.0;
		this.disFirstTouch = new Laya.Vector2(0, 0);
		this.disLastTouch = new Laya.Vector2(0, 0);
		this.isTwoTouch = false;
		this.first = true;
		this.twoFirst = true;
		this.rotate = new Laya.Vector3(0,0,0);

		this.modelRotate = false;//模型默认自转,当用户点击到屏幕时取消自转
		this.modelMove = false;// 模型自动漫游
	}
	onStart(){
		this.scene =  this.owner.parent;
		// this.rigidBody = this.owner._components[1]
		this.rigidBody = this.owner.getComponent(Laya.CharacterController)
		this.firstRotate = new Laya.Quaternion(this.owner.transform.rotation.x,this.owner.transform.rotation.y,this.owner.transform.rotation.z,this.owner.transform.rotation.w);
		this.firstPosition = new Laya.Vector3(this.owner.transform.position.x,this.owner.transform.position.y,this.owner.transform.position.z);
		Laya.stage.on(Laya.Event.CLICK,this,this.toggleMovestate);
	}
	toggleMovestate(){
		this.modelMove = !this.modelMove;
	}
	onUpdate(){
		var mod = this.owner;
		if (this.modelRotate) {
			mod.transform.rotate(new Laya.Vector3(0,0.05,0),false,false);
		}
		if (this.modelMove) {
			let modR = mod.transform.rotationEuler.y / 180 * Math.PI;
			let x = -Math.sin(modR);
			let z = -Math.cos(modR);
			// this.rigidBody.linearVelocity = new Laya.Vector3(x,0,z);
			this.rigidBody.move(new Laya.Vector3(x/40,0,z/40));
		} else {
			// this.rigidBody.linearVelocity = new Laya.Vector3(0,0,0);
			this.rigidBody.move(new Laya.Vector3(0,0,0));
		}
		let touchCount = this.scene.input.touchCount();// 获取触摸点个数
		if (1 === touchCount){
			//判断是否为两指触控,撤去一根手指后引发的touchCount===1
			if(this.isTwoTouch){
				return;
			}
			
			//获取当前的触控点,数量为1
			let touch = this.scene.input.getTouch(0);// 获取触摸点,参数代表索引
			//是否为新一次触碰,并未发生移动
			if (this.first){
				//获取触碰点的位置
				this.lastPosition.x = touch._position.x;
				this.lastPosition.y = touch._position.y;
				this.first = false;
			}
			else{
				//移动触碰点
				let deltaY
				deltaY =  this.lastPosition.y - touch._position.y;

				let deltaX = touch._position.x - this.lastPosition.x;
				this.lastPosition.x = touch._position.x;
				this.lastPosition.y = touch._position.y;
				//根据移动的距离进行旋转	
				this.rotate.setValue(0, deltaX / 50, 0);
				mod.transform.rotate(this.rotate,false,false);
			}
		}
		else if (2 === touchCount){
			this.modelMove = false;
			this.isTwoTouch = true;
			//获取两个触碰点
			let touch = this.scene.input.getTouch(0);
			let touch2 = this.scene.input.getTouch(1);
			//是否为新一次触碰,并未发生移动
			if (this.twoFirst){
				//获取触碰点的位置
				this.disFirstTouch.x = touch.position.x - touch2.position.x;
				this.disFirstTouch.y = touch.position.y - touch2.position.y;
				this.distance = Laya.Vector2.scalarLength(this.disFirstTouch);// 计算标量长度
				// console.log('First Distance',this.distance);

				this.touchAFirstX = touch.position.x;
				this.touchAFirstY = touch.position.y;
				this.touchBFirstX = touch2.position.x;
				this.touchBFirstY = touch2.position.y;

				this.twoFirst = false;
			}
			else{

				// 移动
				this.touchALastX = touch.position.x;
				this.touchALastY = touch.position.y;
				this.touchBLastX = touch2.position.x;
				this.touchBLastY = touch2.position.y;

				let centerFirst = new Laya.Point((this.touchAFirstX + this.touchBFirstX) / 2, (this.touchAFirstY + this.touchBFirstY) / 2); 
				let centerLast = new Laya.Point((this.touchALastX + this.touchBLastX) / 2, (this.touchALastY + this.touchBLastY) / 2);

				let moveX,moveY;
				moveX = (centerFirst.x - centerLast.x) / 100;
				moveY = (centerFirst.y - centerLast.y) / 100;

				let modR = mod.transform.rotationEuler.y / 180 * Math.PI,x,z;
				if (Math.abs(moveY) > Math.abs(moveX)) {
					if (moveY > 0) {// go
					} else if (moveY < 0) {// back
						modR += 180;
					}
				} else {
					if (moveX > 0) {// left
						modR += 90;
					} else if (moveX < 0) {// right
						modR -= 90;
					}
				}
				
				x = -Math.sin(modR);
				z = -Math.cos(modR);
				this.rigidBody.move(new Laya.Vector3(x/40,0,z/40));

				this.touchAFirstX = touch.position.x;
				this.touchAFirstY = touch.position.y;
				this.touchBFirstX = touch2.position.x;
				this.touchBFirstY = touch2.position.y;	
			}	
		}
		else if (0 === touchCount){
			this.first = true;
			this.twoFirst = true;
			this.lastPosition.x = 0;
			this.lastPosition.y = 0;
			this.isTwoTouch = false;
		}
	}
}

Layabox 以人物第一视角在3D空间(模型)中沿着指定路线漫游


export default class AutoGo extends Laya.Script3D{
	constructor(){
		super();
		this.scene = null;
		this.speed = 1;
	}
	
    /**
     * 第一次执行update之前执行,只会执行一次
     */
	onStart(){
		this.scene =  this.owner.parent;
        this.pathConfig = [// 路线配置坐标
            [21,1.5,-0.8],
            [25,1.5,-0.8],
            [25,1.5,11.5],
            [9,1.5,11.5],
            [9,1.5,-0.8],
        ];
        this.nextIndex = 1;
        this.firstRotate = new Laya.Vector3(); this.owner.transform.rotationEuler.cloneTo(this.firstRotate);
        this.firstPos = new Laya.Vector3(); this.owner.transform.position.cloneTo(this.firstPos);
		// this.rigidBody = this.owner._components[1]
		this.rigidBody = this.owner.getComponent(Laya.CharacterController)
	}

	setSpeed(s){
		this.speed = s;
	}
    /**
     * 脚本每次启动后执行,例如被添加到一个激活的对象上或者设置脚本的enabled = true
     */
	 _onEnable(){
		this._onUpdate();
	}

	_onDisable(){
		cancelAnimationFrame(this.animateId);
        this.reset();
	}

    reset(){
        if(this.firstRotate && this.firstPos) {
            this.nextIndex = 1;
			this.owner.transform.rotation = new Laya.Vector3(this.firstRotate.x,this.firstRotate.y,this.firstRotate.z);
			this.owner.transform.position = new Laya.Vector3(this.firstPos.x,this.firstPos.y,this.firstPos.z);
			console.log('重置:',this.owner.transform.rotation,this.owner.transform.position)
		}
    }

    /**
     * 每帧更新时执行
     */
	_onUpdate(){
		var mod = this.owner;
		if (!this.rigidBody || !this.pathConfig){
			this.animateId = requestAnimationFrame(this._onUpdate.bind(this));
			return;
		}
        if (!mod.targetPosition) {
            mod.targetPosition = new Laya.Vector3(mod.transform.position.x,mod.transform.position.y,mod.transform.position.z);
            console.log('起始坐标位置',mod.targetPosition);
            mod.alpha = 0;
        }
        
        var dis = Laya.Vector3.distance(mod.transform.position, mod.targetPosition);
        if (dis < 0.3) {
            this.rigidBody.move(new Laya.Vector3(0,0,0));
            let newTarget = new Laya.Vector3(this.pathConfig[this.nextIndex][0], this.pathConfig[this.nextIndex][1], this.pathConfig[this.nextIndex][2]);

            // 计算转向夹角
            let modR = mod.transform.rotationEuler.y / 180 * Math.PI;
			let x = -Math.sin(modR);
			let z = -Math.cos(modR);
            let aV = new Laya.Vector3(x,0,z);
            let bV = new Laya.Vector3(0,0,0);
            Laya.Vector3.subtract(newTarget, mod.transform.position,bV);

            let cosAlpha = Laya.Vector3.dot(aV,bV) / (Laya.Vector3.scalarLength(aV) * Laya.Vector3.scalarLength(bV));
            let alpha = Math.acos(cosAlpha);
            // console.log(alpha)// 此值始终大于0小于Math.PI;
            if (alpha < -0.1 || alpha > 0.1) {
                let cross = (aV.x * bV.z - bV.x * aV.z);
                let g = cross > 0 ? -0.03 : 0.03;// aV在bV的逆时针方向为-0.03;aV在bV的顺时针方向为0.03;
                mod.transform.rotate(new Laya.Vector3(0,g,0));
                this.animateId = requestAnimationFrame(this._onUpdate.bind(this));
                return;
            }

            // mod.transform.rotate(new Laya.Vector3(0,alpha,0),false,true);   

            // 计算下一段路的单位步伐;
            var dir = new Laya.Vector3();
            Laya.Vector3.subtract(newTarget, mod.transform.position, dir);
            Laya.Vector3.normalize(dir, dir);
            Laya.Vector3.scale(dir, 0.04 * this.speed, dir);
            mod.speed = dir;
            mod.targetPosition = newTarget;
            console.log('当前位置',mod.transform.position);
            console.log('下一目标位置',newTarget);

            if (this.nextIndex === this.pathConfig.length - 1) {
                this.nextIndex = 0;
            } else {
                this.nextIndex++;
            }
        } else {
            // console.log('move');
            this.rigidBody.move(mod.speed);
        }

		this.animateId = requestAnimationFrame(this._onUpdate.bind(this));
	}

}

小程序的3D模型在微信开发者工具上可以用,到真机上不能用的可能原因:

  1. 3D模型中有的文件命名含中文,空格等特殊字符
  2. 纹理不是2的n次幂

模型在开发者工具和IOS中没问题,在安卓上出不来的解决:

var texture = textureLoader.load(tex_url);
texture.minFilter = THREE.LinearFilter;// 解决问题的关键代码
 var material = new THREE.MeshBasicMaterial(
     {
         map: texture
     }
 );

使用Layers隐藏模型

currModel.layers.set(1);
currModel.traverse(item => {
    item.layers.set(1);
});

判断three.js版本号的方式

搜索REVISION即可

推荐链接:

three.js详细学习文档
three.js历史版本代码
three editer
three.js微信小程序127版本demo
three.js微信小程序127demo简化版
three.js微信小程序视频纹理demo

 类似资料: