three.js学习笔记(二十一)——页面加载进度条

介绍

到目前为止,我们页面上只有一块WebGL画布,当其准备好后就即刻显示出来。
本次课程将学习如何添加一个简单的条状加载器,在加载资源时进行填充。
场景将为黑色,只有在全部内容加载完毕才淡出显示。

初设

跟真实渲染一课中一样的设置,画面中间一顶飞行员头盔

import './style.css'
import * as THREE from 'three'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'

/**
 * Loaders
 */
const gltfLoader = new GLTFLoader()
const cubeTextureLoader = new THREE.CubeTextureLoader()

/**
 * Base
 */
// Debug
const debugObject = {}

// Canvas
const canvas = document.querySelector('canvas.webgl')

// Scene
const scene = new THREE.Scene()

/**
 * Update all materials
 */
const updateAllMaterials = () =>
{
    scene.traverse((child) =>
    {
        if(child instanceof THREE.Mesh && child.material instanceof THREE.MeshStandardMaterial)
        {
            // child.material.envMap = environmentMap
            child.material.envMapIntensity = debugObject.envMapIntensity
            child.material.needsUpdate = true
            child.castShadow = true
            child.receiveShadow = true
        }
    })
}

/**
 * Environment map
 */
const environmentMap = cubeTextureLoader.load([
    '/textures/environmentMaps/0/px.jpg',
    '/textures/environmentMaps/0/nx.jpg',
    '/textures/environmentMaps/0/py.jpg',
    '/textures/environmentMaps/0/ny.jpg',
    '/textures/environmentMaps/0/pz.jpg',
    '/textures/environmentMaps/0/nz.jpg'
])

environmentMap.encoding = THREE.sRGBEncoding

scene.background = environmentMap
scene.environment = environmentMap

debugObject.envMapIntensity = 5

/**
 * Models
 */
gltfLoader.load(
    '/models/FlightHelmet/glTF/FlightHelmet.gltf',
    (gltf) =>
    {
        gltf.scene.scale.set(10, 10, 10)
        gltf.scene.position.set(0, - 4, 0)
        gltf.scene.rotation.y = Math.PI * 0.5
        scene.add(gltf.scene)

        updateAllMaterials()
    }
)

/**
 * Lights
 */
const directionalLight = new THREE.DirectionalLight('#ffffff', 3)
directionalLight.castShadow = true
directionalLight.shadow.camera.far = 15
directionalLight.shadow.mapSize.set(1024, 1024)
directionalLight.shadow.normalBias = 0.05
directionalLight.position.set(0.25, 3, - 2.25)
scene.add(directionalLight)

/**
 * 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.set(4, 1, - 4)
scene.add(camera)

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

/**
 * Renderer
 */
const renderer = new THREE.WebGLRenderer({
    canvas: canvas,
    antialias: true
})
renderer.physicallyCorrectLights = true
renderer.outputEncoding = THREE.sRGBEncoding
renderer.toneMapping = THREE.ReinhardToneMapping
renderer.toneMappingExposure = 3
renderer.shadowMap.enabled = true
renderer.shadowMap.type = THREE.PCFSoftShadowMap
renderer.setSize(sizes.width, sizes.height)
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))

/**
 * Animate
 */
const tick = () =>
{
    // Update controls
    controls.update()

    // Render
    renderer.render(scene, camera)

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

tick()

遮罩

首先需要淡化场景,可以给画布canvas的css不透明度opacity设置动画,或者在画布上方放一个黑色div然后一样给其opacity设置动画。但这次我们要在WebGL内部绘制一个覆盖整个渲染器的矩形,并在需要时淡出。

问题是:我们如何在摄像机前方绘制一个矩形?

根据前面所学知识,我们可以绘制一个平面并将其放置于摄像机内部而非场景中,因为摄像机camera也是继承自object3D,但是会有些怪异。
相反,我们将绘制一个不受位置、透视、投影规则影响的平面,使其仅仅只在我们视图前方。

基础平面

先绘制一个平面,加到场景中:

/**
 * Overlay
 */
const overlayGeometry = new THREE.PlaneBufferGeometry(1, 1, 1, 1)
const overlayMaterial = new THREE.MeshBasicMaterial({ color: 0xff0000 })
const overlay = new THREE.Mesh(overlayGeometry, overlayMaterial)
scene.add(overlay)

three.js学习笔记(二十一)——页面加载进度条_第1张图片

填充渲染

我们要这个平面不论摄像机如何移动位置,其一直位于摄像机前方并且填满整个渲染。
为了做到这个效果,我们需要着色器材质ShaderMaterial。

使用着色器材质ShaderMaterial替换基础网格材质MeshBasicMaterial,并使用vertexShader属性和fragmentShader属性编写我们之前学习的默认着色器。
画面没有变化,但我们可以控制着色器了。

const overlayMaterial = new THREE.ShaderMaterial({
    vertexShader: `
        void main()
        {
            gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
        }
    `,
    fragmentShader: `
        void main()
        {
            gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
        }
    `
})

要让平面填充渲染,不需要用到矩阵:

const overlayMaterial = new THREE.ShaderMaterial({
    vertexShader: `
        void main()
        {
            gl_Position = vec4(position, 1.0);
        }
    `,
    fragmentShader: `
        void main()
        {
            gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
        }
    `
})

现在会得到一个矩形在画面正中间,不受任何相机移动造成的影响,因为它没有用到任何矩阵:

这个平面顶点坐标从-0.50.5,因为它的尺寸是1

要得到更大些的矩形,需要坐标从-11,为此我们把PlaneBufferGeometry的尺寸翻倍:

const overlayGeometry = new THREE.PlaneBufferGeometry(2, 2, 1, 1)

现在你会看到整个屏幕都是红色的。

颜色与α值

现在我们把颜色换成黑色:

const overlayMaterial = new THREE.ShaderMaterial({
    // ...
    fragmentShader: `
        void main()
        {
            gl_FragColor = vec4(0.0, 0.0, 0.0, 1.0);
        }
    `
})

three.js学习笔记(二十一)——页面加载进度条_第2张图片
一切回归黑暗,然后我们修改阿尔法值为0.5:

const overlayMaterial = new THREE.ShaderMaterial({
    // ...
    fragmentShader: `
        void main()
        {
            gl_FragColor = vec4(0.0, 0.0, 0.0, 0.5);
        }
    `
})

你会看到一样纯黑色没有变半透明。
记住,我们要给着色器材质开启transparent透明属性:

const overlayMaterial = new THREE.ShaderMaterial({
    transparent: true,
    // ...
})

three.js学习笔记(二十一)——页面加载进度条_第3张图片

uniform

现在让我们添加unifom来实现对阿尔法值的控制:

const overlayMaterial = new THREE.ShaderMaterial({
    // ...
    uniforms:
    {
        uAlpha: { value: 1 }
    },
    // ...
    fragmentShader: `
        uniform float uAlpha;

        void main()
        {
            gl_FragColor = vec4(0.0, 0.0, 0.0, uAlpha);
        }
    `
})

加载

现在我们的遮罩层已经做好了,接下去要弄清楚什么时候画布中的内容全部加载完毕。

虽然场景中只有一个模型,但我们确实加载了许多资源。我们正在加载构成环境贴图的6张图片,模型的几何体,所有模型上的贴图纹理。

而为了加载这些资源,我们使用了GLTFLoader和CubeTextureLoader,它们都可以接收一个LoadingManager作为参数。在学习纹理的时候,其实就接触到LoadingManager了。

下面实例化一个加载管理器LoadingManager并在GLTFLoaderCubeTextureLoader中使用它:

/**
 * Loaders
 */
const loadingManager = new THREE.LoadingManager()
const gltfLoader = new GLTFLoader(loadingManager)
const cubeTextureLoader = new THREE.CubeTextureLoader(loadingManager)

再往加载管理器传入俩个函数参数,第一个函数会战全部内容加载完毕后触发,第二个函数是在加载过程中触发:

const loadingManager = new THREE.LoadingManager(
    // Loaded
    () =>
    {
        console.log('loaded')
    },

    // Progress
    () =>
    {
        console.log('progress')
    }
)

查看控制台,可以看到打印了多个progress和一个loaded。

遮罩层动画

要添加动画让遮罩层淡出,我们将使用GSAP库。
在终端中使用下列命令安装该库:

 npm install --save [email protected]

引入并在加载管理器中使用:

import { gsap } from 'gsap'

// ...

const loadingManager = new THREE.LoadingManager(
    // Loaded
    () =>
    {
        gsap.to(overlayMaterial.uniforms.uAlpha, { duration: 3, value: 0 })
    },

    // ...
)

进度条

设置

回到html文件和css文件添加和设置进度条样式:


<body>
    <canvas class="webgl">canvas>
    <div class="loading-bar">div>
body>
html>
.loading-bar
{
    position: absolute;
    top: 50%;
    width: 100%;
    height: 2px;
    background: #ffffff;
    transform: scaleX(0.3);
    transform-origin: top left;
}

进度条动画

transform属性缩放值设为0,回到js中,在加载管理器的过程函数中控制更新进度条。
过程函数接收三个参数:

  • url : 被加载的项的url。
  • itemsLoaded : 目前已加载项的个数。
  • itemsTotal : 总共所需要加载项的个数。
const loadingManager = new THREE.LoadingManager(
    // ...

    // Progress
    (itemUrl, itemsLoaded, itemsTotal) =>
    {
        console.log(itemUrl, itemsLoaded, itemsTotal)
    }
)

three.js学习笔记(二十一)——页面加载进度条_第4张图片
之后便是基础的js操作了,获取进度条元素,然后根据加载项占比去修改样式缩放值:

const loadingBarElement = document.querySelector('.loading-bar')

const loadingManager = new THREE.LoadingManager(
    // ...

    // Progress
    (itemUrl, itemsLoaded, itemsTotal) =>
    {
        const progressRatio = itemsLoaded / itemsTotal
        loadingBarElement.style.transform = `scaleX(${progressRatio})`
    }
)

为了让加载动画更顺滑些,设置transition属性的过渡时长:

.loading-bar
{
    /* ... */
    transition: transform 0.5s;
}

隐藏进度条

当资源加载完毕后需要隐藏进度条,先设置结束样式

.loading-bar.ended
{
    transform: scaleX(0);
    transform-origin: 100% 0;
    transition: transform 1.5s ease-in-out;
}

当资源加载完毕后添加该样式:

const loadingManager = new THREE.LoadingManager(
    // Loaded
    () =>
    {
        gsap.to(overlayMaterial.uniforms.uAlpha, { duration: 3, value: 0, delay: 1 })

        loadingBarElement.classList.add('ended')
        loadingBarElement.style.transform = ''
    },

    // ...
)

现在进度条会在右边消失,但是动画依旧有改进之处。

首先,第一次渲染场景中的元素需要时间,计算机会暂时冻结。其次,我们在进度条中添加了0.5秒的过渡时长。这也就意味着,当加载函数被触发时,进度条还没有完成到结束状态的转换。

为此可以利用setTimeout(...)

const loadingManager = new THREE.LoadingManager(
    // Loaded
    () =>
    {
        window.setTimeout(() =>
        {
            gsap.to(overlayMaterial.uniforms.uAlpha, { duration: 3, value: 0, delay: 1 })

            loadingBarElement.classList.add('ended')
            loadingBarElement.style.transform = ''
        }, 500)
    },

    // ...
)

你可能感兴趣的:(three.js学习笔记,javascript,three.js,前端)