Skip to content
Go back

Three.js 3D字体,星系制作(二)

Updated:  at  11:44 AM

3d字体

const scene = new THREE.Scene()
const textureLoader = new THREE.TextureLoader()
const matcapTexture = textureLoader.load('textures/matcaps/8.png')

// 字体加载器
const fontLoader = new FontLoader()

fontLoader.load(
    // 字体文件转json:https://gero3.github.io/facetype.js/
    '/fonts/helvetiker_regular.typeface.json',
    (font) =>
    {
        const material = new THREE.MeshMatcapMaterial({ matcap: matcapTexture })

        // 文本
        // 通过减少 curveSegments 和 bevelSegments 属性来保持几何体尽可能低的多边形
        const textGeometry = new TextGeometry(
            'Hello Three.js',
            {
                font: font,
                size: 0.5,
                height: 0.2,
                curveSegments: 12,
                bevelEnabled: true,
                bevelThickness: 0.03,
                bevelSize: 0.02,
                bevelOffset: 0,
                bevelSegments: 5
            }
        )

        // 文本几何体移到坐标原点
        textGeometry.computeBoundingBox()   // 计算几何体的最大最小坐标值
        textGeometry.translate(
            - (textGeometry.boundingBox.max.x - 0.02) * 0.5, // 减掉 bevelSize
            - (textGeometry.boundingBox.max.y - 0.02) * 0.5, // 减掉 bevelSize
            - (textGeometry.boundingBox.max.z - 0.03) * 0.5  // 减掉 bevelThickness
        )

        // 上面移动到原点的代码等同于:
        // textGeometry.center()

        const text = new THREE.Mesh(textGeometry, material)
        scene.add(text)

        // 甜甜圈
        const donutGeometry = new THREE.TorusGeometry(0.3, 0.2, 32, 64)

        for(let i = 0; i < 100; i++)
        {
            const donut = new THREE.Mesh(donutGeometry, material)
            donut.position.x = (Math.random() - 0.5) * 10
            donut.position.y = (Math.random() - 0.5) * 10
            donut.position.z = (Math.random() - 0.5) * 10
            donut.rotation.x = Math.random() * Math.PI
            donut.rotation.y = Math.random() * Math.PI
            const scale = Math.random()
            donut.scale.set(scale, scale, scale)

            scene.add(donut)
        }
    }
)

灯光

// 环境光
const ambientLight = new THREE.AmbientLight()
ambientLight.color = new THREE.Color(0xffffff)
ambientLight.intensity = 0.5
scene.add(ambientLight)

// 平行光 DirectionalLight( color : Color, intensity : Float )
const directionalLight = new THREE.DirectionalLight(0x00fffc, 0.3)
directionalLight.position.set(1, 0.25, 0)
scene.add(directionalLight)

// 半球光 光源直接放置于场景之上,光照颜色从天空光线颜色渐变到地面光线颜色。半球光不能投射阴影。
const hemisphereLight = new THREE.HemisphereLight(0xff0000, 0x0000ff, 0.3)
scene.add(hemisphereLight)

// 点光源
const pointLight = new THREE.PointLight(0xff9000, 0.5, 10, 2)
pointLight.position.set(1, - 0.5, 1)
scene.add(pointLight)

// 平面光光源 光源从一个矩形平面上均匀地发射光线。这种光源可以用来模拟像明亮的窗户或者条状灯光光源。
const rectAreaLight = new THREE.RectAreaLight(0x4e00ff, 2, 1, 1)
rectAreaLight.position.set(- 1.5, 0, 1.5)
rectAreaLight.lookAt(new THREE.Vector3())
scene.add(rectAreaLight)

// 聚光灯(SpotLight) 光线从一个点沿一个方向射出,随着光线照射的变远,光线圆锥体的尺寸也逐渐增大。
const spotLight = new THREE.SpotLight(0x78ff00, 0.5, 10, Math.PI * 0.1, 0.25, 1)
spotLight.position.set(0, 2, 3)
scene.add(spotLight)

spotLight.target.position.x = - 0.75
scene.add(spotLight.target)

// 光源辅助器可以帮助定位和定向光源 辅助器的第二参数是控制其大小
const hemisphereLightHelper = new THREE.HemisphereLightHelper(hemisphereLight, 0.2)
scene.add(hemisphereLightHelper)

const directionalLightHelper = new THREE.DirectionalLightHelper(directionalLight, 0.2)
scene.add(directionalLightHelper)

const pointLightHelper = new THREE.PointLightHelper(pointLight, 0.2)
scene.add(pointLightHelper)

const spotLightHelper = new THREE.SpotLightHelper(spotLight)
scene.add(spotLightHelper)

const rectAreaLightHelper = new RectAreaLightHelper(rectAreaLight)
scene.add(rectAreaLightHelper)

阴影 shadow

// 阴影的显示,首先需要开启阴影功能
renderer.shadowMap.enabled = true

// ...
// 几何体需要开启蒙上阴影
sphere.castShadow = true

// ...
// 地面需要开启接收阴影
plane.receiveShadow = true


/** 添加直射光 **/ 

// 灯光要开启蒙上阴影,来存储shadowMap
directionalLight.castShadow = true

// 灯光的shadowMap
console.log(directionalLight.shadow)

// shadowMap的默认大小是512*512,修改其贴图尺寸可以让阴影更清晰
directionalLight.shadow.mapSize.width = 1024
directionalLight.shadow.mapSize.height = 1024

// Three.js使用相机来进行阴影贴图渲染。这些相机与我们已经使用的相机具有相同的属性。
// 这意味着我们必须定义近和远。它不会真正提高阴影的质量,但它可能会修复看不到阴影或阴影突然被裁剪的错误。
const directionalLightCameraHelper = new THREE.CameraHelper(directionalLight.shadow.camera)
scene.add(directionalLightCameraHelper)

// 设置阴影贴图的相机的near和far属性来保证,场景内物体的阴影都被计算
directionalLight.shadow.camera.near = 1
directionalLight.shadow.camera.far = 6


// 控制相机每侧可以看到的距离,值越小,阴影越精确。但如果它太小,阴影就会被裁剪掉
directionalLight.shadow.camera.top = 2
directionalLight.shadow.camera.right = 2
directionalLight.shadow.camera.bottom = - 2
directionalLight.shadow.camera.left = - 2

// radius 属性控制阴影模糊
directionalLight.shadow.radius = 10


// THREE.BasicShadowMap 性能很好,但质量很差
// THREE.PCFShadowMap 性能较差,但边缘更光滑
// THREE.PCFSoftShadowMap 性能较差,但边缘更柔软
// THREE.VSMShadowMap 更低的性能,更多的约束,可能会产生意想不到的结果
renderer.shadowMap.type = THREE.PCFSoftShadowMap

/** 添加聚光灯 **/ 
const spotLight = new THREE.SpotLight(0xffffff, 0.4, 10, Math.PI * 0.3)

spotLight.castShadow = true

spotLight.position.set(0, 2, 2)
scene.add(spotLight)
scene.add(spotLight.target)

const spotLightCameraHelper = new THREE.CameraHelper(spotLight.shadow.camera)
scene.add(spotLightCameraHelper)

// 降低环境光和直射光强度
const ambientLight = new THREE.AmbientLight(0xffffff, 0.4)
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.4)

// 更改贴图尺寸,保证质量
spotLight.shadow.mapSize.width = 1024
spotLight.shadow.mapSize.height = 1024

// field of view 视场角度 和 远近
spotLight.shadow.camera.fov = 30
spotLight.shadow.camera.near = 1
spotLight.shadow.camera.far = 6
// 隐藏灯光helper
spotLightCameraHelper.visible = false

/** 增加点光源 **/ 
const pointLight = new THREE.PointLight(0xffffff, 0.3)

pointLight.castShadow = true

pointLight.position.set(- 1, 1, 0)
scene.add(pointLight)
// ...

使用blender等三维软件烘焙生成的阴影贴图


// 关闭光源
directionalLight.castShadow = false
spotLight.castShadow = false
pointLight.castShadow = false
renderer.shadowMap.enabled = false

// 加载一张烘焙的贴图,/static/textures/bakedShadow.jpg
const textureLoader = new THREE.TextureLoader()
const bakedShadow = textureLoader.load('/textures/bakedShadow.jpg')

// 当然,如果球体或灯光移动,阴影并不会随着移动
const plane = new THREE.Mesh(
    new THREE.PlaneGeometry(5, 5),
    new THREE.MeshBasicMaterial({
        map: bakedShadow
    })
)

粒子 particle

粒子的一个简单示例

const particlesGeometry = new THREE.BufferGeometry()
const particlesMaterial = new THREE.PointsMaterial({
    color: '#ff88cc',       // 控制粒子颜色
    size: 0.02,             // 控制粒子尺寸
    sizeAttenuation: true   // 指定远处粒子的尺寸是否应小于近距离粒子
})

const count = 500
const positions = new Float32Array(count * 3)
const colors = new Float32Array(count * 3)

for(let i = 0; i < count * 3; i++)
{
    positions[i] = (Math.random() - 0.5) * 10
    colors[i] = Math.random()
}

particlesGeometry.setAttribute('position', new THREE.BufferAttribute(positions, 3))
// 需要注意 PointsMaterial 的 color 属性也会影响几何所设置的颜色,若不需要受影响,需去除color属性
particlesGeometry.setAttribute('color', new THREE.BufferAttribute(colors, 3))


// 设置粒子纹理贴图
const textureLoader = new THREE.TextureLoader()
const particleTexture = textureLoader.load('/textures/particles/2.png')
// 使用透明激活透明度
particlesMaterial.transparent = true
// 值得注意的是,材质的color属性也会影响贴图的颜色
// particlesMaterial.map = particleTexture
// 使用 alphaMap 属性上的纹理而不是贴图
particlesMaterial.alphaMap = particleTexture

// 粒子的绘制顺序与它们创建的顺序相同,WebGL并不知道哪个粒子在另一个粒子的前面
// alphaTest是一个介于0和1之间的值,使WebGL能够根据像素的透明度知道何时不渲染该像素。
// 默认情况下,该值为0,这意味着像素无论如何都会被渲染。如果我们使用一个较小的值,例如0.001,则当alpha为0时,像素将不会被渲染
// particlesMaterial.alphaTest = 0.001

// 绘制时,WebGL会测试正在绘制的内容是否比已绘制的更接近,这称为 depthTest,设置 false,粒子不再区分前后
// articlesMaterial.depthTest = false

// WebGL会测试正在绘制的内容是否比已经绘制的更接近。所绘制内容的深度存储在我们所说的深度缓冲区中。
// 我们可以告诉WebGL不要在该深度缓冲区中写入粒子,而不是不测试粒子是否比该深度缓冲区中的粒子更接近
particlesMaterial.depthWrite = false

// 通过更改blending属性,我们可以告诉WebGL不仅要绘制像素,还要将该像素的颜色添加到已绘制的像素的颜色中
particlesMaterial.blending = THREE.AdditiveBlending

const particles = new THREE.Points(particlesGeometry, particlesMaterial)
scene.add(particles)

// 关于three的时钟
const clock = new THREE.Clock()
const tick = () =>
{
    // ...
    // 轨道控制的缓动需要每帧调用
    controls.update()

    // 获取当前运行时间
    const elapsedTime = clock.getElapsedTime()

    for(let i = 0; i < count; i++)
    {
        let i3 = i * 3

        // 实现波浪的效果,让每个粒子的y根据其x坐标来进行设置,并且加入了时间参数
        const x = particlesGeometry.attributes.position.array[i3]
        particlesGeometry.attributes.position.array[i3 + 1] = Math.sin(elapsedTime + x)
    }
    particlesGeometry.attributes.position.needsUpdate = true

    // 渲染画面
    renderer.render(scene, camera)
    // 每一帧重新调用
    window.requestAnimationFrame(tick)
    
    // ...
}

星系的产生

import * as THREE from 'three'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'
import * as dat from 'lil-gui'

const gui = new dat.GUI()
const canvas = document.querySelector('canvas.webgl')
const scene = new THREE.Scene()

/**
 * 星系参数
 */
const params = {
    count: 50000,               // 控制粒子数量
    size: 2,                    // 粒子大小
    radius: 5,                  // 星云半径
    branches: 3,                  // 星云分支数
    spin: 0.7,                  // 扭转幅度
    randomness: 0.1,            // 粒子离散程度
    randomnessPower: 6,                   // 距离中心的离散幅度(数值越大,距离中心远的离散程度越大)
    insideColor: '#ba33cc',     // 中心颜色
    outsideColor: '#4c6fd6',    // 外边缘颜色
    colorVariety: 1,            // 颜色复杂度
    colorOffset: 1,             // 色值位移
}

let gemotry = null
let material = null
let points = null

function color(color, radius, colorVariety = 2, colorOffset = 1) {
    const a = 0.5;
    const b = 0.5;
    const c = { r: 0.333, g: 0.667, b: 0.739 }
    return a + b * Math.sin(Math.PI * colorVariety * (c[color] + radius + colorOffset))
}

const generateGalaxy = () => {
    // 每次重新构建,清除之前产生的粒子
    if (points) {
        gemotry.dispose()
        material.dispose()
        scene.remove(points)
    }
    let positions = new Float32Array(params.count * 3);
    let colors = new Float32Array(params.count * 3);

    // const inside = new THREE.Color(params.initColor)
    // const outside = new THREE.Color(params.finalColor)

    gemotry = new THREE.BufferGeometry()

    for (var i = 0; i < params.count; i++) {
        var i3 = i * 3

        // 使粒子均匀分布在最大长度为radius的线上
        const radius = params.radius * Math.random()
        // 根据分支数计算出每个分支的基础角度
        var branchAngle = (i % params.branches) / params.branches * Math.PI * 2
        // 粒子的旋转角度,根据与中心的距离来判断,让直线旋转起来
        var spinAngle = params.spin * radius

        // 粒子的离散程度,根据与中心的距离远近,越远离散程度越大
        const randomX = Math.pow(Math.random(), params.randomnessPower) * (Math.random() < 0.5 ? -1 : 1) * radius * params.randomness
        const randomY = Math.pow(Math.random(), params.randomnessPower) * (Math.random() < 0.5 ? -1 : 1) * radius * params.randomness
        const randomZ = Math.pow(Math.random(), params.randomnessPower) * (Math.random() < 0.5 ? -1 : 1) * radius * params.randomness

        positions[i3] = Math.cos(branchAngle + spinAngle) * radius + randomX
        positions[i3 + 1] = randomY
        positions[i3 + 2] = Math.sin(branchAngle + spinAngle) * radius + randomZ

        // let mixedColor = inside.clone();
        // mixedColor.lerp(outside, radius / params.radius)
        // colors[i3    ] = mixedColor.r
        // colors[i3 + 1] = mixedColor.g
        // colors[i3 + 2] = mixedColor.b

        // 生成随机颜色
        colors[i3] = color('r', radius / params.radius, params.colorVariety, params.colorOffset)
        colors[i3 + 1] = color('g', radius / params.radius, params.colorVariety, params.colorOffset)
        colors[i3 + 2] = color('b', radius / params.radius, params.colorVariety, params.colorOffset)
    }
    gemotry.setAttribute('position', new THREE.BufferAttribute(positions, 3))
    gemotry.setAttribute('color', new THREE.BufferAttribute(colors, 3))

    material = new THREE.PointsMaterial({
        size: params.size,
        sizeAttenuation: false,
        depthWrite: false,
        blending: THREE.AdditiveBlending,
        vertexColors: true
    })

    points = new THREE.Points(gemotry, material)
    scene.add(points)
}

gui.add(params, 'count').min(100).max(1000000).step(100).onFinishChange(generateGalaxy)
gui.add(params, 'size').min(0.001).max(0.1).step(0.001).onFinishChange(generateGalaxy)
gui.add(params, 'radius').min(0.01).max(20).step(0.01).onFinishChange(generateGalaxy)
gui.add(params, 'branches').min(2).max(20).step(1).onFinishChange(generateGalaxy)
gui.add(params, 'spin').min(- 5).max(5).step(0.001).onFinishChange(generateGalaxy)
gui.add(params, 'randomness').min(0).max(2).step(0.001).onFinishChange(generateGalaxy)
gui.add(params, 'randomnessPower').min(1).max(10).step(0.001).onFinishChange(generateGalaxy)
// gui.addColor(params, 'insideColor').onFinishChange(generateGalaxy)
// gui.addColor(params, 'outsideColor').onFinishChange(generateGalaxy)
gui.add(params, 'colorVariety').min(1).max(10).step(0.1).onFinishChange(generateGalaxy)
gui.add(params, 'colorOffset').min(1).max(10).step(0.1).onFinishChange(generateGalaxy)

generateGalaxy()

/**
 * Sizes
 */
const sizes = {
    width: window.innerWidth,
    height: window.innerHeight
}

window.addEventListener('resize', () => {
    // Update sizes
    sizes.width = window.innerWidth
    sizes.height = window.innerHeight

    // Update camera
    camera.aspect = sizes.width / sizes.height
    camera.updateProjectionMatrix()

    // Update renderer
    renderer.setSize(sizes.width, sizes.height)
    renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
})

/**
 * Camera
 */
// Base camera
const camera = new THREE.PerspectiveCamera(75, sizes.width / sizes.height, 0.1, 100)
camera.position.x = 3
camera.position.y = 3
camera.position.z = 3
scene.add(camera)

// Controls
const controls = new OrbitControls(camera, canvas)
controls.enableDamping = true

/**
 * Renderer
 */
const renderer = new THREE.WebGLRenderer({
    canvas: canvas
})
renderer.setSize(sizes.width, sizes.height)
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))

/**
 * Animate
 */
const clock = new THREE.Clock()

const tick = () => {
    const elapsedTime = clock.getElapsedTime()

    // Update controls
    controls.update()

    // Render
    renderer.render(scene, camera)

    // Call tick again on the next frame
    window.requestAnimationFrame(tick)
}

tick()


Previous Post
Three.js 使用模型,材质,灯光(一)
Next Post
Three.js Webgl动画,物理碰撞引擎(三)