08.06. 图元练习示例

06 图元练习示例

上一篇文章中,列举了 Three.js 中内置的 22 种图元(Primitives),那么本文将重点练习,尝试使用这些图元。

复习一下图元的概念:图元 就是 Three.js 中内置的 几何体。

示例代码目标

  1. 将内置的所有种类图元,除 TextBufferGeometry 以外,其他 21 种图元 逐一练习

    TextBufferGeometry 比较特殊,会在稍后一节专门讲解

  2. 在练习中,尽量都使用图元的 BufferGeometry 类型,不做过多自定义设置

  3. 最终将所有创建出的形状放置在同一场景中,并进行渲染

示例代码组织逻辑

  1. 创建 src/components/hello-primitives/ 目录,用来存放本示例所有代码
  2. 在该目录下,创建 index.tsx 文件,用来构建 Three.js 3 大基础元素:场景、镜头、渲染器
  3. 在该目录下,创建 index.scss 文件,用来添加需要用到的 CSS 样式
  4. 在该目录下,分别创建 MyBox.ts、MyCircle.ts ... 等文件,用来依次创建不同的图元实例
  5. 在 index.stx 文件中,引入各个图元实例,并加入场景中进行渲染
  6. 修改 App.tsx,将之前编写的 <HelloThreejs /> 替换为我们本示例的组件 <HelloPrimitives />

本示例主要用来练习 Three.js,对于示例中使用到的一些 ES6、React Hooks、TypeScript 等相关知识,若非必要,一般情况下就不再过多讲解说明。

示例代码

index.tsx

import { useRef, useEffect, useCallback } from 'react'
import * as Three from 'three'

import './index.scss'

import myBox from './my-box'
import myCircle from './my-circle'
import myCone from './my-cone'
import myCylinder from './my-cylinder'
import myDodecahedron from './my-dodecahedron'
import myEdges from './my-edges'
import myExtrude from './my-extrude'
import myIcosahedron from './my-icosahedron'
import myLathe from './my-lathe'
import myOctahedron from './my-octahedron'
import myParametric from './my-parametric'
import myPlane from './my-plane'
import myPolyhedron from './my-polyhedron'
import myRing from './my-ring'
import myShape from './my-shape'
import mySphere from './my-sphere'
import myTetrahedron from './my-tetrahedron'
import myTorus from './my-torus'
import myTorusKnot from './my-torus-knot'
import myTube from './my-tube'
import myWireframe from './my-wireframe'

const meshArr: (Three.Mesh | Three.LineSegments)[] = []

const HelloPrimitives = () => {
    const canvasRef = useRef<HTMLCanvasElement>(null)
    const rendererRef = useRef<Three.WebGLRenderer | null>(null)
    const cameraRef = useRef<Three.PerspectiveCamera | null>(null)

    const createMaterial = () => {
        const material = new Three.MeshPhongMaterial({ side: Three.DoubleSide })

        const hue = Math.floor(Math.random() * 100) / 100 //随机获得一个色相
        const saturation = 1 //饱和度
        const luminance = 0.5 //亮度

        material.color.setHSL(hue, saturation, luminance)

        return material
    }

    const createInit = useCallback(
        () => {

            if (canvasRef.current === null) {
                return
            }

            meshArr.length = 0 //以防万一,先清空原有数组

            //初始化场景
            const scene = new Three.Scene()
            scene.background = new Three.Color(0xAAAAAA)

            //初始化镜头
            const camera = new Three.PerspectiveCamera(40, 2, 0.1, 1000)
            camera.position.z = 120
            cameraRef.current = camera

            //初始化渲染器
            const renderer = new Three.WebGLRenderer({ canvas: canvasRef.current as HTMLCanvasElement })
            rendererRef.current = renderer

            //添加 2 盏灯光
            const light0 = new Three.DirectionalLight(0xFFFFFF, 1)
            light0.position.set(-1, 2, 4)
            scene.add(light0)

            const light1 = new Three.DirectionalLight(0xFFFFFF, 1)
            light0.position.set(1, -2, -4)
            scene.add(light1)

            //获得各个 solid 类型的图元实例,并添加到 solidPrimitivesArr 中
            const solidPrimitivesArr: Three.BufferGeometry[] = []
            solidPrimitivesArr.push(myBox, myCircle, myCone, myCylinder, myDodecahedron)
            solidPrimitivesArr.push(myExtrude, myIcosahedron, myLathe, myOctahedron, myParametric)
            solidPrimitivesArr.push(myPlane, myPolyhedron, myRing, myShape, mySphere)
            solidPrimitivesArr.push(myTetrahedron, myTorus, myTorusKnot, myTube)

            //将各个 solid 类型的图元实例转化为网格,并添加到 primitivesArr 中
            solidPrimitivesArr.forEach((item) => {
                const material = createMaterial() //随机获得一种颜色材质
                const mesh = new Three.Mesh(item, material)
                meshArr.push(mesh) //将网格添加到网格数组中
            })

            //获得各个 line 类型的图元实例,并添加到 meshArr 中
            const linePrimitivesArr: Three.BufferGeometry[] = []
            linePrimitivesArr.push(myEdges, myWireframe)

            //将各个 line 类型的图元实例转化为网格,并添加到 meshArr 中
            linePrimitivesArr.forEach((item) => {
                const material = new Three.LineBasicMaterial({ color: 0x000000 })
                const mesh = new Three.LineSegments(item, material)
                meshArr.push(mesh)
            })

            //定义物体在画面中显示的网格布局
            const eachRow = 5 //每一行显示 5 个
            const spread = 15 //行高 和 列宽

            //配置每一个图元实例,转化为网格,并位置和材质后,将其添加到场景中
            meshArr.forEach((mesh, index) => {
                //我们设定的排列是每行显示 eachRow,即 5 个物体、行高 和 列宽 均为 spread 即 15
                //因此每个物体根据顺序,计算出自己所在的位置
                const row = Math.floor(index / eachRow) //计算出所在行
                const column = index % eachRow //计算出所在列

                mesh.position.x = (column - 2) * spread //为什么要 -2 ?
                //因为我们希望将每一行物体摆放的单元格,依次是:-2、-1、0、1、2,这样可以使每一整行物体处于居中显示
                mesh.position.y = (2 - row) * spread

                scene.add(mesh) //将网格添加到场景中
            })

            //添加自动旋转渲染动画
            const render = (time: number) => {
                time = time * 0.001
                meshArr.forEach(item => {
                    item.rotation.x = time
                    item.rotation.y = time
                })

                renderer.render(scene, camera)
                window.requestAnimationFrame(render)
            }
            window.requestAnimationFrame(render)
        },
        [canvasRef],
    )

    const resizeHandle = () => {
        //根据窗口大小变化,重新修改渲染器的视椎
        if (rendererRef.current === null || cameraRef.current === null) {
            return
        }

        const canvas = rendererRef.current.domElement
        cameraRef.current.aspect = canvas.clientWidth / canvas.clientHeight
        cameraRef.current.updateProjectionMatrix()
        rendererRef.current.setSize(canvas.clientWidth, canvas.clientHeight, false)
    }

    //组件首次装载到网页后触发,开始创建并初始化 3D 场景
    useEffect(() => {
        createInit()
        resizeHandle()
        window.addEventListener('resize', resizeHandle)
        return () => {
            window.removeEventListener('resize', resizeHandle)
        }
    }, [canvasRef, createInit])

    return (
        <canvas ref={canvasRef} className='full-screen' />
    )
}

export default HelloPrimitives

my-box.ts

import { BoxBufferGeometry } from "three"

const width = 8
const height = 8
const depth = 8

const myBox = new BoxBufferGeometry(width, height, depth)

export default myBox

my-circle.ts

import { CircleBufferGeometry } from "three"

const radius = 7
const segments = 24

const myCircle = new CircleBufferGeometry(radius,segments)

export default myCircle

由于图元实在是太多,这里省略掉一些图元相关代码......

my-wireframe.ts

import { BoxBufferGeometry, WireframeGeometry } from "three";

const width = 8;
const height = 8;
const depth = 8

const myWireframe = new WireframeGeometry(new BoxBufferGeometry(width, height, depth))

export default myWireframe

想要查看完整的图元示例代码,可以访问:https://threejsfundamentals.org/threejs/threejs-primitives.html

本文示例中的代码源头,都来源于上面那个网页,尽管该网页 JS 中创建图元使用的是 JS 而非 TS。

你可以通过查看该页面中内嵌的 JS 代码,来补齐其他图元对应的创建写法。

图元种类这么多,本文只是带着你一块过一遍,具体每一个图元具体的参数含义,以后总会慢慢了解的。

下一节,我们将讲述一下 TextBufferGeometry 的用法。