three.js 9 - TransformControls

import * as THREE from 'three';
import { TransformControls } from "three/examples/jsm/controls/TransformControls";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";

/**
 * 3d Controls 控制器,变换控制器(TransformControls)
 * https://threejs.org/docs/index.html#examples/zh/controls/TransformControls
 */
export class ThreeDoc9ControlTransform {
    constructor(canvasId) {
        this.work(canvasId);
    }

    work(canvasId) {
        // 创建 3d 场景
        const scene = new THREE.Scene();
        scene.background = new THREE.Color(0x9e9e9e);

        const renderer = new THREE.WebGLRenderer();
        renderer.setSize(window.innerWidth, window.innerHeight);
        // 最后一步很重要,我们将renderer(渲染器)的dom元素(renderer.domElement)添加到我们的HTML文档中。这就是渲染器用来显示场景给我们看的<canvas>元素。
        document.body.appendChild(renderer.domElement);

        // 点材质(PointsMaterial)
        this.getPointsMaterial(scene);

        // AxesHelper  3个坐标轴的对象.
        this.addAxesHelper(scene);
        // 半球光(HemisphereLight)
        this.addHemisphereLight(scene);
        // 网格辅助对象
        let size = 20;
        const gridHelper = new THREE.GridHelper(size, 10, 0x444444, 0xffffff);
        scene.add(gridHelper);


        const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
        // 设置相机位置
        camera.position.x = 5;
        camera.position.y = 6;
        camera.position.z = 15;
        camera.lookAt(0, 0, 0);

        // 添加物体
        let geometry = new THREE.BoxGeometry(2, 2, 2);
        let material = new THREE.MeshStandardMaterial({ color: 0x049EF4 });
        let obj = new THREE.Mesh(geometry, material);
        obj.position.set(0, 1, 0);
        scene.add(obj);

        // 添加鼠标操作视图
        let orbC = this.initMouseControl(scene, camera, renderer);

        // 添加拖动事件,返回平移控件对象
        let transformControls = this.initDragControl(scene, camera, renderer, orbC);

        // 添加操作面板,切换控制器类型
        this.addControlPanel(renderer, scene, camera, transformControls);

        // 添加点击事件,设置物体控制器显示
        this.addClickEvent(scene, camera, transformControls, renderer);

        renderer.render(scene, camera);
    }

    /**
     * AxesHelper
     * 用于简单模拟3个坐标轴的对象.
     * 红色代表 X 轴. 绿色代表 Y 轴. 蓝色代表 Z 轴.
     * AxesHelper( size : Number )
     * size -- (可选的) 表示代表轴的线段长度. 默认为 1.
     */
    addAxesHelper(scene) {
        const axesHelper = new THREE.AxesHelper(12);
        scene.add(axesHelper);
    }

    /**
     * 半球光(HemisphereLight) - 喜欢这个光
     * 光源直接放置于场景之上,光照颜色从天空光线颜色渐变到地面光线颜色。
     * 半球光不能投射阴影。
     *
     * HemisphereLight( skyColor : Integer, groundColor : Integer, intensity : Float )
     * skyColor - (可选参数) 天空中发出光线的颜色。 缺省值 0xffffff。
     * groundColor - (可选参数) 地面发出光线的颜色。 缺省值 0xffffff。
     * intensity - (可选参数) 光照强度。 缺省值 1。
     */
    addHemisphereLight(scene) {
        const light = new THREE.HemisphereLight(0xffffbb, 0x080820, 50);
        scene.add(light);
        light.position.set(0, -20, 0);
        // const helper = new THREE.HemisphereLightHelper(light, 3);
        // scene.add(helper);
    }

    /**
     * 点材质(PointsMaterial)
     * Points使用的默认材质。
     * PointsMaterial( parameters : Object )
     * parameters - (可选)用于定义材质外观的对象,具有一个或多个属性。 材质的任何属性都可以从此处传入(包括从Material继承的任何属性)。
     *
     * 属性color例外,其可以作为十六进制字符串传递,默认情况下为 0xffffff(白色),内部调用Color.set(color)。
     */
    getPointsMaterial(scene) {
        const vertices = [];

        for (let i = 0; i < 10000; i++) {

            const x = THREE.MathUtils.randFloatSpread(2000);
            const y = THREE.MathUtils.randFloatSpread(2000);
            const z = THREE.MathUtils.randFloatSpread(2000);

            vertices.push(x, y, z);

        }

        const geometry = new THREE.BufferGeometry();
        geometry.setAttribute('position', new THREE.Float32BufferAttribute(vertices, 3));

        const material = new THREE.PointsMaterial({ color: 0xffffff });

        const points = new THREE.Points(geometry, material);

        scene.add(points);
    }

    /**
     * 鼠标操作
     */
    initMouseControl(scene, camera, renderer) {
        // 添加鼠标操作
        let controls = new OrbitControls(camera, renderer.domElement);
        controls.addEventListener('change', () => {
            renderer.render(scene, camera);
        });
        return controls;
    }

    // 添加拖拽控件
    initDragControl(scene, camera, renderer, orbC) {

        /**
         * 变换控制器(TransformControls)
         * 该类可提供一种类似于在数字内容创建工具(例如Blender)中对模型进行交互的方式,来在3D空间中变换物体。 和其他控制器不同的是,变换控制器不倾向于对场景摄像机的变换进行改变。
         * TransformControls 期望其所附加的3D对象是场景图的一部分。
         *
         * TransformControls( camera : Camera, domElement : HTMLDOMElement )
         * camera: 被控制的摄像机。
         * domElement: 用于事件监听的HTML元素。
         * 创建一个新的 TransformControls 实例。
         *
         * 事件
         * change : 如果发生了任何类型的改变(对象或属性的改变)则触发该事件。 属性改变是单独的事件,你也可以为此添加单独的事件监听;
         * 该事件类型为"propertyname-changed"(“属性名称-changed”)。
         * mouseDown : 如果指针(鼠标/触摸)为活动状态则触发该事件。
         * mouseUp : 如果指针(鼠标/触摸)不再为活动状态则触发该事件。
         * objectChange : 如果被控制的3D对象发生改变则触发该事件。。
         *
         * 属性
         * 共有属性请参见其基类Object3D。
         * .axis : String : 当前变换轴。
         * .camera : Camera : 渲染场景的摄像机。
         *
         * .domElement : HTMLDOMElement : 该 HTMLDOMElement 用于监听鼠标/触摸事件,该属性必须在构造函数中传入。
         * 在此处改变它将不会设置新的事件监听。
         *
         * .dragging : Boolean : 当前是否正在拖动。只读属性。
         * .enabled : Boolean : 是否启用控制器。默认为true。
         * .mode : String : 当前的变换模式。可能的值包括"translate"、"rotate" 和 "scale"。默认为translate。
         * .object : Object3D : 正在被控制的3D对象。
         *
         * .rotationSnap : Number : 默认情况下,3D对象是可以被连续旋转的。如果你将该值设为一个数值(弧度),
         * 则你将可以定义每次旋转3D对象时的步幅。 默认为null。
         *
         * .showX : Boolean : x轴手柄是否显示。默认为true。
         * .showY : Boolean : y轴手柄是否显示。默认为true。
         * .showZ : Boolean : z轴手柄是否显示。默认为true。
         * .size : Number : 手柄UI(轴/平面)的大小。默认为1。
         * .space : String : 定义了在哪种坐标空间中进行变换。可能的值有"world" 和 "local"。默认为world。
         * .translationSnap : Number : 默认情况下,3D对象是可以被连续平移的。如果你将该值设为一个数值(世界单位),则你将可以定义每次平移3D对象时的步幅。 默认为null。
         *
         * 方法
         * 共有方法请参见其基类Object3D。
         *
         * .attach ( object : Object3D ) : TransformControls
         * object: 应当变换的3D对象。
         *
         * 设置应当变换的3D对象,并确保控制器UI是可见的。
         *
         * .detach () : TransformControls
         * 从控制器中移除当前3D对象,并确保控制器UI是不可见的。
         *
         * .dispose () : undefined
         * 若不再需要该控制器,则应当调用此函数。
         *
         * .getRaycaster () : Raycaster
         * 返回用于用户交互的 Raycaster 对象。 此对象在所有实例之间共享 变换控件。 如果您设置 TransformControls 的 .layers 属性,您还需要 使用匹配值设置 Raycaster 上的 .layers 属性,否则设置 TransformControls 不会按预期工作。
         *
         * .getMode () : String
         * 返回变换模式。
         *
         * .setMode ( mode : String ) : undefined
         * mode: 变换模式。
         *
         * 设置变换模式。
         *
         * .setRotationSnap ( rotationSnap : Number ) : undefined
         * rotationSnap: 旋转捕捉步幅。
         *
         * 设置旋转捕捉。
         *
         * .setSize ( size : Number ) : undefined
         * size: 手柄UI的大小。
         *
         * 设置手柄UI的大小。
         *
         * .setSpace ( space : String ) : undefined
         * space: 应用变换的坐标空间。
         *
         * 设置应用变换的坐标空间。
         *
         * .setTranslationSnap ( translationSnap : Number ) : undefined
         * translationSnap: 平移捕捉步幅。
         *
         * 设置平移捕捉。
         * @type {TransformControls}
         */
        let transformControls = new TransformControls(camera, renderer.domElement);
        scene.add(transformControls);
        // 监听改变,则更新界面
        transformControls.addEventListener("change", () => {
            renderer.render(scene, camera);
        });
        // 变换控制器监听 mousedown,禁用 鼠标拖拽
        transformControls.addEventListener("mouseDown", () => {
            orbC.enabled = false;
        });
        //
        transformControls.addEventListener("mouseUp", () => {
            orbC.enabled = true;
        });

        return transformControls;
    }

    /**
     * 添加操作面板,按钮控制光源对象切换,添加平移控件关联
     * @param renderer
     * @param scene
     * @param camera
     * @param transformControls 平移控制器对象
     */
    addControlPanel(renderer, scene, camera, transformControls) {
        let ele = `
            <div class="control-panel">
                <button class="mode-translate">translate</button>
                <button class="mode-rotate">rotate</button>
                <button class="mode-scale">scale</button>
            </div>
        `;
        $('body').append(ele);
        let cols = ['translate', 'rotate', 'scale'];
        for(let i = 0; i< cols.length; i++){
            // 绑定按钮事件
            $('.mode-' + cols[i]).on('click', function (e) {
                // 控制模式改变:cols[i]
                transformControls.mode = cols[i];
                // renderer.render(scene, camera);
            });
        }
    }

    /**
     * 添加鼠标点击判断,是否点击了物体
     * @param scene
     * @param camera
     * @param transformControls
     */
    addClickEvent(scene, camera, transformControls, renderer) {
        /**
         * 使用官方的THREE.Raycaster
         * THREE.Raycaster是three.js中的射线类,其实现监听的原理是由相机位置为射线起点,由鼠标位置为射线方向发射射线,
         * 其穿过的所有几何体都会被监测到。
         * @type {*[]}
         */
        let intersects = []; //几何体合集
        const pointer = new THREE.Vector2();
        // 给 canvas 加监听点击事件
        document.getElementsByTagName('canvas')[0].addEventListener('click', meshOnClick);
        let raycaster = new THREE.Raycaster();
        function meshOnClick(event) {

            //geometrys 为需要监听的Mesh合集,可以通过这个集合来过滤掉不需要监听的元素例如地面天空
            let geometrys = [];
            for (let i = 0; i < scene.children.length; i++) {
                console.log(scene.children[i].isLight);
                // 物体 和 光源都监听点击
                if (scene.children[i].isMesh || scene.children[i].isLight) {
                    geometrys.push(scene.children[i]);
                }
            }
            console.log(geometrys);
            pointer.x = (event.clientX / window.innerWidth) * 2 - 1;
            pointer.y = -(event.clientY / window.innerHeight) * 2 + 1;
            raycaster.setFromCamera(pointer, camera);
            // true为不拾取子对象
            intersects = raycaster.intersectObjects(geometrys, true);
            // 被射线穿过的几何体为一个集合,越排在前面说明其位置离端点越近,所以直接取[0]
            console.log(intersects);
            if (intersects.length > 0) {
                transformControls.attach(intersects[0].object);
            } else {
                //若没有几何体被监听到,可以做一些取消操作,判断不到光源点击,直接移除控制不可行
                // transformControls.detach();
                // 当前控制器操控的对象
                // console.log(transformControls.object);
                console.log(1);
            }
            renderer.render(scene, camera);

        }
    }
}

 

posted @ 2022-06-06 16:58  名字不好起啊  阅读(1721)  评论(2编辑  收藏  举报