到目前为止,我们页面上只有一块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)
我们要这个平面不论摄像机如何移动位置,其一直位于摄像机前方并且填满整个渲染。
为了做到这个效果,我们需要着色器材质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.5
到0.5
,因为它的尺寸是1
。
要得到更大些的矩形,需要坐标从-1
到1
,为此我们把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);
}
`
})
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,
// ...
})
现在让我们添加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
并在GLTFLoader
和CubeTextureLoader
中使用它:
/**
* 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中,在加载管理器的过程函数中控制更新进度条。
过程函数接收三个参数:
const loadingManager = new THREE.LoadingManager(
// ...
// Progress
(itemUrl, itemsLoaded, itemsTotal) =>
{
console.log(itemUrl, itemsLoaded, itemsTotal)
}
)
之后便是基础的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)
},
// ...
)