ThreeJS掘金最通俗入门指南-像素绘画案例

489 阅读5分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第2天,点击查看活动详情

官方案例链接: three.js examples (threejs.org)

效果预览

image.png

步骤拆分

  • 定义变量,定义场景,相机,渲染器,操作平面,射线选取,预放置材质,已放置材质的变量
  • 初始化场相机,定义相机类型为透视相机,视野范围为45度,宽高比为屏幕宽高比,设置近视距离为1,最远观测距离为1000
  • 初始化场景,设置场景的颜色为0xf0f0f0
  • 初始化与预放置材质,初始化该材质为基础网格材质,这种材质不会反射光线,并且在传递参数的时候设置这个材质为透明材质,当满足某个条件的时候他才不透明。
  • 初始化放置材质,设置长宽高为50,设置他为一种非光泽化的镜面材质,MeshLambertMaterial,这种材质可以很好的模拟一些表面,但是不能模拟具有镜面高光的光泽表面。
  • 初始化网格辅助器,设置网格辅助器的宽度,和每一行分成了多少个格子,然后加入到场景当中去。
  • 初始化镭射选取器和鼠标指针Pointer。
  • 初始化一个实际的可以被访问到的平面,然后将该平面覆盖网格辅助器
  • 初始化光照,设置场景反射光为0x606060,设置直射光,直射光设置为白光。
  • 初始化渲染器,渲染器为WebGL渲染器,将渲染器的domElement添加到页面中去。
  • 初始化监听器,鼠标移动监听器,鼠标点击监听器,键盘点击监听器,键盘抬起监听器。
    • 鼠标移动: 设置pointer的x,y与鼠标移动到的位置相对应,然后通过摄像机和鼠标的位置更新镭射器的位置。获取镭射器光线照射到了哪个场景中的材质,如果说实际选取到了某个材质的话,将他调整位置到gridHelper的格子上显示。
    • 鼠标点击:大体步骤和鼠标移动一致。区别在于我们会判断是否鼠标点击的时候按下了键盘上的shift按键,如果按下了的话,那么我们就走删除逻辑,否则我们就走新增逻辑
    • 键盘点击: 判断是否按下shift,isShift这个变量设置为true
    • 键盘抬起:判断是否按下shift,isShift这个变量设置为false

构建代码模板

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <!-- Import maps polyfill -->
    <!-- Remove this when import maps will be widely supported -->
    <script async src="https://unpkg.com/es-module-shims@1.3.6/dist/es-module-shims.js"></script>

    <script type="importmap">
			{
				"imports": {
					"three": "../build/three.module.js"
				}
			}
		</script>

    <script type="module">
        import * as THREE from 'three'
        let camera,scene,renderer
        let plane
        let pointer,raycaster,isShiftDown=false;
        let cubeGeo,cubeMaterial
        const objects=[]
        init()
        render()
        function init(){
            
        }
        function render(){

        }
    </script>    
</body>
</html>

初始化相机

camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 1, 10000);
camera.position.set(500, 800, 1300);
camera.lookAt(0, 0, 0);

初始化场景

scene = new THREE.Scene();
scene.background = new THREE.Color(0xf0f0f0);

初始化浮动预选方块

const rollOverGeo = new THREE.BoxGeometry(50, 50, 50);
rollOverMaterial = new THREE.MeshBasicMaterial({ color: 0xff0000, opacity: 0.5, transparent: true });
rollOverMesh = new THREE.Mesh(rollOverGeo, rollOverMaterial);
scene.add(rollOverMesh);

初始化实际放置方块

cubeGeo = new THREE.BoxGeometry(50, 50, 50);
cubeMaterial = new THREE.MeshLambertMaterial({ color: 0xfeb74c, map: new THREE.TextureLoader().load('./textures/square-outline-textured.png') });

初始化网格辅助器

const gridHelper = new THREE.GridHelper(1000, 20);
scene.add(gridHelper);

初始化镭射选点&鼠标指针

raycaster = new THREE.Raycaster();
pointer = new THREE.Vector2();

初始化不可见的平面对象

const geometry = new THREE.PlaneGeometry(1000, 1000);
geometry.rotateX(- Math.PI / 2);
plane = new THREE.Mesh(geometry, new THREE.MeshBasicMaterial({ visible: false }));
scene.add(plane);
objects.push(plane);

初始化场景灯光

const ambientLight = new THREE.AmbientLight(0x606060);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff);
directionalLight.position.set(1, 0.75, 0.5).normalize();
scene.add(directionalLight);

初始化渲染器,场景渲染到页面

renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);

增加事件监听器

document.addEventListener('pointermove', onPointerMove);
document.addEventListener('pointerdown', onPointerDown);
document.addEventListener('keydown', onDocumentKeyDown);
document.addEventListener('keyup', onDocumentKeyUp);

鼠标指针预选取事件

function onPointerMove(event) {

            pointer.set((event.clientX / window.innerWidth) * 2 - 1, - (event.clientY / window.innerHeight) * 2 + 1);

            raycaster.setFromCamera(pointer, camera);

            const intersects = raycaster.intersectObjects(objects, false);

            if (intersects.length > 0) {

                const intersect = intersects[0];
                console.log(intersect)
                rollOverMesh.position.copy(intersect.point).add(intersect.face.normal);
                rollOverMesh.position.divideScalar(50).floor().multiplyScalar(50).addScalar(25);

                render();

            }

        }

鼠标点击放置事件

function onPointerDown(event) {

            pointer.set((event.clientX / window.innerWidth) * 2 - 1, - (event.clientY / window.innerHeight) * 2 + 1);

            raycaster.setFromCamera(pointer, camera);

            const intersects = raycaster.intersectObjects(objects, false);

            if (intersects.length > 0) {

                const intersect = intersects[0];

                // delete cube

                if (isShiftDown) {

                    if (intersect.object !== plane) {

                        scene.remove(intersect.object);

                        objects.splice(objects.indexOf(intersect.object), 1);

                    }

                    // create cube

                } else {

                    const voxel = new THREE.Mesh(cubeGeo, cubeMaterial);
                    voxel.position.copy(intersect.point).add(intersect.face.normal);
                    voxel.position.divideScalar(50).floor().multiplyScalar(50).addScalar(25);
                    scene.add(voxel);

                    objects.push(voxel);

                }

                render();

            }

        }

键盘按键按下事件

function onDocumentKeyDown(event) {

            switch (event.keyCode) {

                case 16: isShiftDown = true; break;

            }

        }

键盘按键松开事件

function onDocumentKeyUp(event) {

            switch (event.keyCode) {

                case 16: isShiftDown = false; break;

            }

        }

思考这个场景中使用到的ThreeJS API

初始化灯光

  • 直射光:directionalLight.position.set(1, 0.75, 0.5).normalize();
    • normalize: 将该向量转换为单位向量(unit vector), 也就是说,将该向量的方向设置为和原向量相同,但是其长度

镭射选取器

  • 射线与鼠标同步: raycaster.setFromCamera(pointer, camera);
    • setFromCamera: 将pointer也就是鼠标指针的位置往场景中照射一道射线,射线穿过物体,然后进行选取。
  • 获取射线穿过的物体: raycaster.intersectObjects
    • intersectObjects: 射线穿过的物品

向量设置

  • 设置预选取方块的位置
    • rollOverMesh.position.copy(intersect.point).add(intersect.face.normal);
      • copy: 相当于将intersect的vector向量赋值给rollOverMesh的position
      • add:将intersect的face.normal这个点的x|y|z都依次加到rollOverMesh的x|y|z上面去
    • rollOverMesh.position.divideScalar(50).floor().multiplyScalar(50).addScalar(25);
      • divideScalar:除法,x|y|z都除以50
      • floor: 向下取整
      • multiplyScalar: 乘法,x|y|z都乘50
      • addScalar:加法,x|y|z都加25

在场景中删除/新增操作

  • 判断是否按下Shift

    • 如果说按下Shift再进行点击,那么就是删除
    • 没有按下Shift进行点击,那么就是新增。
  • 删除: 先从场景中移除元素,然后再在Objects数组中移除元素

    • 从场景中移除元素:scene.remove(intersect.object);
    • 从保存的元素数组中移除元素:objects.splice(objects.indexOf(intersect.object), 1);
  • 新增: 将预选取的代码复制一份然后直接添加到场景中去就好了。

    const voxel = new THREE.Mesh(cubeGeo, cubeMaterial);
    voxel.position.copy(intersect.point).add(intersect.face.normal);
    voxel.position.divideScalar(50).floor().multiplyScalar(50).addScalar(25);
    scene.add(voxel);
    objects.push(voxel);
    

【ThreeJS掘金最通俗入门指南-像素绘画案例】 到此结束,如果您觉得对您有帮助的话,可以点赞收藏一下哦。您的支持就是对我最大的鼓励,能让我得到更多的推送流量,帮助到更多的人,最后感谢您花费宝贵的时间认真的阅读这篇文章。如果说您有什么需要我改进的建议欢迎到评论区一起和我讨论❤❤❤