原生微信小程序使用three.js 实现外部obj,fbx,gltf模型导入以及模型点击交互弹框

2,730 阅读5分钟
  1. 项目初期

前期查阅大量资料寻找适用于原生微信小程序的threejs案例,但大都不符合预期;后来在微信开放社区找到一个插件, three-weixin,解决了目前项目的需求

看演示

1f39694e-151a-4a35-99c4-d752fe917556.gif

  1. 插件安装

推荐使用 ThreeX 方式安装,安装完成之后按照插件文档提示进行开发配置

image.png

  1. 开发注意事项

由于微信官方限制主包大小,开发时一定要进行分包,否则项目预览会提示体积过大;

 "subpackages": [{
        "root": "ThreeX",
        "pages": [
            "pages/cube",
            "pages/obj",
            "pages/gltf",
            "pages/fbx",
            "pages/selectionBox"
        ]
    }],
 "plugins": {
        "ThreeX": {
            "version": "1.0.5",
            "provider": "wx5d6376b4fc730db9",
            "export": "threex.js"
        }
    },
  • 项目根目录

image.png

  1. 代码演示
  • 加载gltf格式 wxml文件
<!--ThreeX/pages/selectionBox.wxml-->
<canvas id="canvas_webgl" type="webgl" disable-scroll="true" bindtouchcancel="webgl_touch" bindtouchend="webgl_touch" bindtouchmove="webgl_touch" bindtouchstart="webgl_touch" />

//封装的弹框
<popup id='popup' title='{{popupTitle}}' content_Text='{{Popupcontent}}' bind:close="_close">
</popup>
  • js文件
// ThreeX/pages/selectionBox.js
import {
    document,
    window,
    requestAnimationFrame,
    cancelAnimationFrame,
    Event,
    core,
    performance
} from 'dhtml-weixin';
const THREE = requirePlugin('ThreeX');
import {
    OrbitControls
} from './jsm/controls/OrbitControls.js';
import {
    GLTFLoader
} from './jsm/loaders/GLTFLoader.js';
import Stats from './jsm/libs/stats.module.js';

var requestId
Page({
    data: {
        popupTitle: '标题',
        Popupcontent: '内容',
    },
    onUnload() {
        cancelAnimationFrame(requestId, this.canvas)
        this.worker && this.worker.terminate()
        setTimeout(() => {
            if (this.renderer instanceof THREE.WebGLRenderer) {
                this.renderer.dispose()
                this.renderer.forceContextLoss()
                this.renderer.context = null
                this.renderer.domElement = null
                this.renderer = null
            }
        }, 0)
    },
    // 触摸事件
    webgl_touch(e) {
        const web_e = Event.fix(e)
        // console.log(web_e, 'web_e');
        window.dispatchEvent(web_e)
        document.dispatchEvent(web_e)
        this.canvas.dispatchEvent(web_e)
    },
    // 页面加载初始化
    async onLoad() {
        const canvas3d = this.canvas = await document.createElementAsync("canvas", "webgl")
        var that = this
        let camera, scene, renderer, controls, stats, sprite;
        let flag = false
        init();
        animate();
        document.addEventListener('pointerup', onPointerDown);
        const raycaster = new THREE.Raycaster();
        const mouse = new THREE.Vector2();


        // 3D模型点击事件
        function onPointerDown(event) {
            event.preventDefault();
            // 标准设备横坐标
            mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
            // 标准设备纵坐标
            mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
            // 创建射线投射器对象
            raycaster.setFromCamera(mouse, camera);
            // 返回射线选中的对象 第二个参数如果不填 默认是false
            // 计算物体和射线的交点
            const intersects = raycaster.intersectObjects(scene.children, true);
            // 数组大于0 表示有相交对象
            if (intersects.length > 0) {
                const object = intersects[0].object;
                console.log(intersects, 'intersects');
                console.log(object.name);
                if (object.name) {
                    that.setData({
                        popupTitle: object.name,
                        Popupcontent: object.uuid,
                    })
                    that.showPopup()
                }
            }
        }
        // 初始化场景并加载模型
        function init() {
            const container = document.createElement('div');
            document.body.appendChild(container);
            // 构建一个透视投影的相机
            // fov 摄像机视锥体垂直视野角度。视界 大部分是 30-90 比如游戏就可以调节视野大小
            // aspect 摄像机视锥体长宽比。
            // near 摄像机视锥体近端面。距离相机多近就不渲染了?
            // far 摄像机视锥体远端面。距离相机多远就不渲染了?
            camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 20);
            // 相机位置
            camera.position.set(200, 30, 500);
            // 创建场景
            scene = new THREE.Scene();
            // 向场景添加标签
            // addInfo()

            // model加载模型
            new GLTFLoader()
                .setPath('https://public-object-service.oss-cn-shenzhen.aliyuncs.com/threejs/gltf/car/')
                .load('scene.gltf', function (gltf) {
                    // console.log(gltf, 'gltf-car');
                    scene.add(gltf.scene);
                });
            // 构建渲染器 WebGLRenderer
            renderer = that.renderer = new THREE.WebGLRenderer({
                canvas: canvas3d,
                antialias: true
            });

            // 设置背景颜色和透明度
            renderer.setClearColor(0xeeeeee, 0.5);
            // 设置显示比例
            renderer.setPixelRatio(window.devicePixelRatio);
            // 制定渲染器的宽高
            renderer.setSize(window.innerWidth, window.innerHeight);
            // appendChild(Node)这个方法一般是在指定元素节点的最后一个子节点之后添加节点
            container.appendChild(renderer.domElement);
            stats = new Stats();
            container.appendChild(stats.dom);
            scene.background = new THREE.Color(0xbbbbbb);
            // 加载控制器
            controls = new OrbitControls(camera, renderer.domElement);
            controls.enableDamping = true;
            controls.minDistance = 1;
            controls.maxDistance = 10;
            controls.target.set(0, 0.35, 0);
            controls.update();
            window.addEventListener('resize', onWindowResize);
        }
        // 监听控制器滑动事件
        //controls.addEventListener('change', render)
        //controls.addEventListener("end", unlock);
       
        // 给模型添加标签信息
        function addInfo() {
            const tipTexture = new THREE.TextureLoader().load('https://onekit.cn/examples/textures/crate.gif');
            const spriteMaterial = new THREE.SpriteMaterial({
                map: tipTexture
            });
            sprite = new THREE.Sprite(spriteMaterial);
            sprite.scale.set(0.1, 0.1, 1);
            sprite.position.set(0.6230932411915222, 1.035837173219155, 0.2790637427236902); // 设置标签位置
            sprite.content = '111'
            scene.add(sprite); // 添加到场景中
        }
        // 自适应
        function onWindowResize() {
            camera.aspect = window.innerWidth / window.innerHeight;
            camera.updateProjectionMatrix();
            renderer.setSize(window.innerWidth, window.innerHeight);
        }
        // 动画
        function animate() {
            requestId = requestAnimationFrame(animate);
            controls.update(); // required if damping enabled
            render();
            stats.update();
        }
        // 导出场景
        function render() {
            flag = true
            renderer.render(scene, camera);
        }
    },
    // 弹框绑定事件
    onReady() {
        //获得popup组件
        this.popup = this.selectComponent("#popup");
    },
    // 显示弹框
    showPopup() {
        this.popup.showPopup();
    },
    // 取消弹框
    _close() {
        console.log('你点击了关闭按钮');
        this.popup.hidePopup();
    },
})
  • 加载fbx js文件

image.png

// webgl/webgl_loader_fbx_nurbs.js
import {
    document,
    window,
    requestAnimationFrame,
    cancelAnimationFrame,
    Event,
    core,
    performance
} from 'dhtml-weixin';
const THREE = requirePlugin('ThreeX');
import Stats from './jsm/libs/stats.module.js';
import {
    OrbitControls
} from './jsm/controls/OrbitControls.js';
import {
    FBXLoader
} from './jsm/loaders/FBXLoader.js';
var requestId
Page({
    onUnload() {
        cancelAnimationFrame(requestId, this.canvas)
        this.worker && this.worker.terminate()
        setTimeout(() => {
            if (this.renderer instanceof THREE.WebGLRenderer) {
                this.renderer.dispose()
                this.renderer.forceContextLoss()
                this.renderer.context = null
                this.renderer.domElement = null
                this.renderer = null
            }
        }, 0)
    },
    webgl_touch(e) {
        const web_e = Event.fix(e)
        window.dispatchEvent(web_e)
        document.dispatchEvent(web_e)
        this.canvas.dispatchEvent(web_e)
    },
    async onLoad() {
        const canvas3d = this.canvas = await document.createElementAsync("canvas", "webgl")
        var that = this
        let camera, scene, renderer, stats;
        let mesh
        init();
        animate();
        document.addEventListener('pointerdown', onPointerDown);
        const raycaster = new THREE.Raycaster();
        const mouse = new THREE.Vector2();
        // 3D模型点击事件
        function onPointerDown(event) {
            // 标准设备横坐标
            mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
            // 标准设备纵坐标
            mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
            // 创建射线投射器对象
            raycaster.setFromCamera(mouse, camera);
            // 返回射线选中的对象 第二个参数如果不填 默认是false
            const intersects = raycaster.intersectObjects(scene.children, true);
            // console.log(intersects, 'intersects');
            if (intersects.length > 0) {
                const object = intersects[0].object;
                console.log(object.name);
            }
        }

        function init() {
            const container = document.createElement('div');
            document.body.appendChild(container);
            // 创建相机
            camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 1000);
            // 设置相机位置
            camera.position.set(200, 30, 500);
            scene = new THREE.Scene();
            camera.lookAt(scene.position);
            // 加载纹理图
            const texture = new THREE.TextureLoader().load('https://public-object-service.oss-cn-shenzhen.aliyuncs.com/threejs/fbx/test/textures/desert_house_high_res.jpg');
            // 创建环境光
            const ambientLight = new THREE.AmbientLight(0x404040, 2);
            scene.add(ambientLight);
            // HemisphereLight(半球光光源)
            // const hemiLight = new THREE.HemisphereLight(0xffffff, 0x444444);
            // hemiLight.position.set(1, 1, 1);
            // scene.add(hemiLight);
            // // 创建一个简单的方向光
            const dirLight = new THREE.DirectionalLight(0xffffff, 1);
            dirLight.position.set(20, 20, 20);
            dirLight.castShadow = true;
            //设置相机渲染面积
            dirLight.shadow.camera.near = 0.01;
            dirLight.shadow.camera.far = 60;
            dirLight.shadow.camera.top = 22;
            dirLight.shadow.camera.bottom = -22;
            dirLight.shadow.camera.left = -35;
            dirLight.shadow.camera.right = 35;
            //设置阴影分辨率
            dirLight.shadow.mapSize.width = 2048; // default
            dirLight.shadow.mapSize.height = 2048; // default
            //阴影限制
            dirLight.shadow.radius = 1;
            scene.add(dirLight);
            // grid网格
            // 网格助手模块
            // GridHelper( size:网格的大小, divisions:跨网格的分割数, color1:中心线的颜色, color2:网格线条的颜色 )
            const gridHelper = new THREE.GridHelper(38, 38, 0x303030, 0x303030);
            scene.add(gridHelper);
            // stats检测器
            stats = new Stats();
            container.appendChild(stats.dom);
            // model加载模型
            const loader = new FBXLoader();
            loader.load('https://public-object-service.oss-cn-shenzhen.aliyuncs.com/threejs/fbx/222.fbx', function (object) {
                // console.log(object, 'object');
                mesh = object
                const material = new THREE.MeshLambertMaterial({
                    map: texture
                });
                mesh.traverse(function (obj) {
                    // console.log(obj, "obj");
                    obj.material = material;
                    if (obj instanceof THREE.Mesh) {
                        obj.rotation.z -= 0.03
                        obj.castShadow = true;
                        obj.receiveShadow = true;
                    }
                });
                //设置模型大小
                mesh.scale.set(0.1, 0.1, 0.1);
                scene.add(mesh);
            });
            // 创建渲染器
            renderer = that.renderer = new THREE.WebGLRenderer({
                canvas: canvas3d,
                antialias: true,
                alpha: true
            });
            // 设置渲染器的像素比例是浏览器的像素比例
            renderer.setPixelRatio(window.devicePixelRatio);
            // 设置渲染器的初始颜色
            renderer.setClearColor(new THREE.Color(0xeeeeee));
            // 设置输出canvas画面的大小
            renderer.setSize(window.innerWidth, window.innerHeight);
            // 追加canvas 元素到div元素中。
            container.appendChild(renderer.domElement);
            // 设置背景色
            scene.background = new THREE.Color(0xbbbbbb);
            // 加载控制器
            const controls = new OrbitControls(camera, renderer.domElement);
            // 默认焦点位置是世界坐标的原点,可以通过control.target.set()进行设置
            controls.target.set(0, 12, 0);
            controls.update();
            window.addEventListener('resize', onWindowResize);
        }

        // 窗口自适应大小
        function onWindowResize() {
            camera.aspect = window.innerWidth / window.innerHeight;
            camera.updateProjectionMatrix();
            renderer.setSize(window.innerWidth, window.innerHeight);
        }
        // 设置动画
        function animate() {
            requestId = requestAnimationFrame(animate);
            renderer.render(scene, camera);
            // 设置动画
            // mesh.rotation.y += 0.01;
            // console.log(mesh, 'mesh');
        }
    }
})
  1. 关于模型

推荐前往 sketchfab.com/ 下载各种模型;唯一不好的是需要科学上网

  1. 总结

好了,分享只有这些啦! 第一次发文章,有问题的可以发一下讨论呀!