Skip to content

87. 实战:Three.js Editor(四)

Published:

上节实现了根据全局 store 里的 json 渲染 3d 场景的 mesh:

image.png

image.png

这节继续来完善。

首先实现点击菜单,添加 Mesh 和 Light 的功能

image.png

各个组件都可以从全局 Store 里访问和修改 json:

image.png

所以我们只需要在 Menu 组件里点击添加的时候,修改 json 添加对应的 mesh

然后在 Main 组件里同步通知 Three.js 去更新场景就好了。

image.png

const { addMesh } = useThreeStore();

function handleClick(e) {
    addMesh(e.key);
}

拿到 store 里的 addMesh 方法。

点击菜单项的时候,调用 addMesh 来添加一个 mesh。

然后我们在 Properties 组件里展示下 json:

import { useThreeStore } from "../../store";

function Properties() {
    const { data } = useThreeStore();
        
    return <div className="Properties">
        <pre>
        {JSON.stringify(data, null, 2)}
        </pre>
    </div>
}

export default Properties;

试一下:

2025-04-28 16.04.30.gif

点击添加立方体是有可以的,别的我们还没实现。

我们实现下这 2 种 Mesh 和 2 种 Light 的添加:

改下 store/index.js 里的 addMesh 方法:

addMesh(type) {
    function addItem(creator) {
        set(state => {
            return {
                data: {
                    ...state.data,
                    meshArr: [
                        ...state.data.meshArr,
                        creator()
                    ]
                }
            }
        })
    }
    if(type === 'Box') {
        addItem(createBox);
    } else if(type === 'Cylinder') {
        addItem(createCylinder);
    }
}

和前面差不多,只不过多了一个 Cylinder 类型。

然后创建 createCylinder 方法:

image.png

function createCylinder() {
    const newId = Math.random().toString().slice(2, 8);
    return {
        id: newId,
        type: 'Cylinder',
        name: 'Cylinder' + newId,
        props: {
            radiusTop: 200,
            radiusBottom: 200,
            height: 300,
            material: {
                color: 'orange',
            },
            position: {
                x: 0,
                y: 0,
                z: 0
            }
        }
    }
}

store 里添加 mesh 搞定了,我们还需要把它渲染出来:

把之前渲染的逻辑注释掉:

image.png

我们直接在组件里写:

image.png

const sceneRef = useRef();
sceneRef.current = scene;

首先用 useRef 来保存 scene。

然后在 data 变化的时候去更新 scene:

image.png

import { MeshTypes, useThreeStore } from "../../store";
import * as THREE from 'three';
useEffect(() => {
    const scene = sceneRef.current;

    data.meshArr.forEach(item => {
        if(item.type === MeshTypes.Box) {
            const { width, height, depth, material: { color }, position} = item.props;
            let mesh = scene.getObjectByName(item.name);

            if(!mesh) {
                const geometry = new THREE.BoxGeometry(width, height, depth);
                const material = new THREE.MeshPhongMaterial({
                    color
                });
                mesh = new THREE.Mesh(geometry, material);
            }    

            mesh.name = item.name;
            mesh.position.copy(position)
            scene.add(mesh);
        } else if(item.type === MeshTypes.Cylinder) {
            const { radiusTop, radiusBottom, height, material: { color }, position} = item.props;
            let mesh = scene.getObjectByName(item.name);

            if(!mesh) {
                const geometry = new THREE.CylinderGeometry(radiusTop, radiusBottom, height);
                const material = new THREE.MeshPhongMaterial({
                    color
                });
                mesh = new THREE.Mesh(geometry, material);
            }
            mesh.name = item.name;
            mesh.position.copy(position)
            scene.add(mesh);
        }
    })
}, [data]);

data 变化的时候,遍历 data.meshArr 来新增或者更新 mesh。

根据 name 来查找 mesh,如果找到了就直接更新属性,否则就创建一个新的。

(这里写的时候 scene 没类型提示,因为为了简化,我们没用 ts。你可以在 init.js 里写完之后复制过来,或者你可以用 typescript 来写)

把 meshArr 清空:

image.png

试下效果:

2025-04-28 16.35.47.gif

没啥问题。

然后来做一下选中的功能。

实现点击需要用到 RayCaster,这个我们写过很多次了。

image.png

renderer.domElement.addEventListener('click', (e) => {
    const y = -((e.offsetY / height) * 2 - 1);
    const x = (e.offsetX / width) * 2 - 1;

    const rayCaster = new THREE.Raycaster();
    rayCaster.setFromCamera(new THREE.Vector2(x, y), camera);

    const intersections = rayCaster.intersectObjects(scene.children);

    if(intersections.length) {
      const obj = intersections[0].object;
      obj.material.color.set('green');
    }
});

这段逻辑的具体含义如果忘了,可以看一下射线与点击选中 3D 场景的物体 这节。

我们点击选中的物体设置绿色。

试一下:

2025-04-28 16.55.52.gif

然后我们用后期描边效果来表示选中:

image.png

const composer = new EffectComposer(renderer);

const renderPass = new RenderPass(scene, camera);
composer.addPass(renderPass);

const v = new THREE.Vector2(window.innerWidth, window.innerWidth);
const outlinePass = new OutlinePass(v, scene, camera);
outlinePass.pulsePeriod = 1;
composer.addPass(outlinePass);

function render(time) {
    composer.render();
    // renderer.render(scene, camera);
    requestAnimationFrame(render);
}

创建效果合成器 EffectComposer,然后添加两个后期通道 RenderPass、OutlinePass。

OutlinePass 设置闪烁周期是 1s

然后在渲染循环里调用 composer.render

这样点击的时候修改 selectedObjects 来给物体添加描边:

image.png

if(intersections.length) {
  const obj = intersections[0].object;
//   obj.material.color.set('green');
    outlinePass.selectedObjects = [obj];
} else {
    outlinePass.selectedObjects = [];
}

试一下:

2025-04-28 17.04.03.gif

这样,描边效果就加上了。

只是明显感觉场景变暗了。

这个是加了后期通道后的常见问题,后期通道那节讲过,加一下伽马校正就好了:

image.png

const gammaPass= new ShaderPass(GammaCorrectionShader);
composer.addPass(gammaPass);

2025-04-28 17.07.11.gif

这样,颜色就正常了。

案例代码上传了小册仓库

总结

这节我们实现了点击菜单项添加对应 Mesh 的功能,并且加上了点击选中的描边效果。

添加对应 Mesh 就是往 json 里添加对应的对象,然后渲染的时候把它渲染出来。

而选中描边就是通过 RayCaster 和后期通道 OutlinePass 实现的。

下节我们继续来做删除、编辑功能。

评论