背景
之前写过一个关于辉光的文章,全场景辉光(传送门),但是有些时候,我们的场景只需要一部分模型辉光,这就是本文的背景。还是基于ThreeJS实现。
原理
简单来说,就是做两个合成器(Composer),一个用于辉光(glowComposer),一个用于混合辉光和正常渲染(finalComposer)。在创建一个集合对象,用来存储要辉光的模型,主要是用于区分辉光和不辉光的模型,反过来存不辉光的模型也可以,或者用标志来区分也行,宗旨就是将辉光和不辉光的模型能分开识别即可。官网例子使用的是图层layer,因为我觉得图层不利于理解,也比较麻烦,所以本文直接使用数组来代替。
在场景render的时候,先将不辉光的模型材质设置为黑色,因为辉光一般背景是黑色,模型设置为黑色后,辉光也没有效果。然后先执行辉光合成器的render进行渲染,此时场景渲染辉光,而不需要辉光的模型因为设置了黑色,所以也看不出来,到此,场景中需要辉光的模型就实现了辉光,而不需要辉光的模型目前是看不见的,所以接下来,我们要将材质设置为黑色的模型还原成原本的材质颜色,再执行最终合成器的render进行渲染,因为finalComposer合成器中不包含辉光特效,所以第二次redner后,只渲染了正常不辉光的模型,而第一次渲染辉光的模型也被混合在一起了,所以场景最终就实现了一部分模型辉光,一部分不辉光。
需要注意的是,将模型设置为黑色后,还原的时候不仅仅是要还原颜色,还要将材质也还原。另外最终合成器使用了一些webGL代码,如果感兴趣可以研究一下,如果单纯实现效果,直接忽略即可。
效果
蓝色六面体为不辉光模型,红色六面体为辉光模型。
核心代码
// 创建场景封装对象(场景、光源等于本例无关的内容)
const ts = new TS('container')
// 辉光模型集合
const glows = []
// 初始化模型
initModel()
// 初始合成器
const { finalComposer, glowComposer } = initComposer()
// 渲染逻辑
const render = () => {
// 不辉光的先变黑
ts.scene.traverse(darkenMaterial)
// 渲染辉光合成器
glowComposer.render()
// 还原不会光的材质
ts.scene.traverse(restoreMaterial)
// 渲染最终合成器
finalComposer.render()
requestAnimationFrame(this.render)
})
/**
* 非辉光材质颜色设置黑色
*/
function darkenMaterial (obj) {
const material = obj.material
if (material && !glows.includes(obj)) {
obj.originalMaterial = obj.material
const Proto = Object.getPrototypeOf(material).constructor
obj.material = new Proto({ color: ts.scene.background })
}
}
/**
* 还原材质
*/
function restoreMaterial (obj) {
if (!obj.originalMaterial) return
obj.material = obj.originalMaterial
delete obj.originalMaterial
}
/**
* 初始化通道
*/
function initComposer () {
// 辉光参数
const params = {
// 强度
bloomStrength: 1.5,
// 阈值
bloomThreshold: 0,
// 半径
bloomRadius: 0
}
// 渲染通道
const renderPass = new RenderPass(ts.scene, ts.camera)
// 辉光通道
const bloomPass = new UnrealBloomPass(new THREE.Vector2(window.innerWidth, window.innerHeight), params.bloomStrength, params.bloomRadius, params.bloomThreshold)
// 辉光合成器
const glowComposer = new EffectComposer(ts.renderer)
glowComposer.renderToScreen = false
glowComposer.addPass(renderPass)
glowComposer.addPass(bloomPass)
// 最终通道
const finalPass = new ShaderPass(
new THREE.ShaderMaterial({
uniforms: {
baseTexture: { value: null },
bloomTexture: { value: glowComposer.renderTarget2.texture }
},
vertexShader: WebGL.VERTEX_SHADER,
fragmentShader: WebGL.FRAGMENT_SHADER,
defines: {}
}), 'baseTexture'
)
finalPass.needsSwap = true
// 最终合成器
const finalComposer = new EffectComposer(ts.renderer)
finalComposer.addPass(renderPass)
finalComposer.addPass(finalPass)
// UI调试
const gui = new GUI()
window.gui = gui
gui.add(params, 'bloomThreshold', 0.0, 1.0).step(0.01).name('阈值').onChange(function (value) {
bloomPass.threshold = Number(value)
})
// 强度 在0-10之间可正常看到物体,超过10会因光线过强而看不见物体,步长建议0.01
gui.add(params, 'bloomStrength', 0, 10).step(0.01).name('强度').onChange(function (value) {
bloomPass.strength = Number(value)
})
gui.add(params, 'bloomRadius', 0.0, 1.0).step(0.01).name('半径').onChange(function (value) {
bloomPass.radius = Number(value)
})
return { finalComposer, glowComposer }
}
/**
* 初始化模型
*/
function initModel () {
const boxGeometry = new THREE.BoxGeometry(100, 100, 100)
// 创建地面材质
const boxMaterial = new THREE.MeshPhongMaterial({
color: '#68a5f1',
side: 0
})
// 场景随机创建20个盒子
for (let i = 0; i < 20; i++) {
const cubeMesh = new THREE.Mesh(boxGeometry.clone(), boxMaterial.clone())
cubeMesh.position.setY(50)
cubeMesh.castShadow = true
cubeMesh.receiveShadow = true
cubeMesh.position.set(random(-500, 400), random(-0, 500), random(-500, 400))
// 偶数的为红色,放入辉光容器,奇数为蓝色,不辉光
if (i % 2 === 0) {
glows.push(cubeMesh)
cubeMesh.material.color = new THREE.Color('#fc2d5d')
}
ts.scene.add(cubeMesh)
}
}
webGL代码:
class WebGL {
static get VERTEX_SHADER () {
return '\t\t\tvarying vec2 vUv;\n' +
'\n' +
'\t\t\tvoid main() {\n' +
'\n' +
'\t\t\t\tvUv = uv;\n' +
'\n' +
'\t\t\t\tgl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );\n' +
'\n' +
'\t\t\t}'
}
static get FRAGMENT_SHADER () {
return '\t\t\tuniform sampler2D baseTexture;\n' +
'\t\t\tuniform sampler2D bloomTexture;\n' +
'\n' +
'\t\t\tvarying vec2 vUv;\n' +
'\n' +
'\t\t\tvoid main() {\n' +
'\n' +
'\t\t\t\tgl_FragColor = ( texture2D( baseTexture, vUv ) + vec4( 1.0 ) * texture2D( bloomTexture, vUv ) );\n' +
'\n' +
'\t\t\t}'
}
}
export { WebGL }