前言
效果噪点图
为了可以自定义能量球的效果,这里使用外部加载来的噪点图做纹理,省去用代码写特效的过程。
主要代码
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Energy shield</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no" />
<style>
body {
margin: 0;
padding: 0;
}
</style>
</head>
<body>
<script type="module">
import * as THREE from '../../build/three.module.js'
import * as TWEEN from '../../build/tween.esm.js'
import Stats from '../jsm/libs/stats.module.js'
import { OrbitControls } from '../jsm/controls/OrbitControls.js'
import { shader as depthVertexShader } from './shaders/depth-vs.js'
import { shader as depthFragmentShader } from './shaders/depth-fs.js'
import { shader as shieldVertexShader } from './shaders/shield-vs.js'
import { shader as shieldFragmentShader } from './shaders/shield-fs.js'
// renderer
const renderer = new THREE.WebGLRenderer({ antialias: true })
renderer.setPixelRatio(window.devicePixelRatio)
renderer.shadowMap.enabled = true
renderer.shadowMap.type = THREE.PCFSoftShadowMap
renderer.outputEncoding = THREE.sRGBEncoding
renderer.gammaFactor = 2.2
document.body.append(renderer.domElement)
// stats
const stats = new Stats()
document.body.appendChild(stats.domElement)
// scene
const scene = new THREE.Scene()
const camera = new THREE.PerspectiveCamera(60, 1, 0.1, 100)
camera.position.set(0, 1, 2)
// control
const controls = new OrbitControls(camera, renderer.domElement)
controls.minDistance = 0.5
controls.maxDistance = 10
controls.enableDamping = true
controls.dampingFactor = 0.05
controls.screenSpacePanning = true
controls.autoRotate = false
// cube
const cubeGeometry = new THREE.BoxBufferGeometry(0.4, 0.4, 0.4)
const cubeMaterial = new THREE.MeshStandardMaterial({
color: 'rgb(100, 70, 30)',
roughness: 0.4,
metalness: 0.0,
side: THREE.DoubleSide
})
const cube = new THREE.Mesh(cubeGeometry, cubeMaterial)
cube.castShadow = cube.receiveShadow = true
cube.position.set(-0.6, 0.2, -0.6)
cube.rotation.set(-Math.PI / 2, 0, 0)
scene.add(cube)
// ground
const groundGeometry = new THREE.PlaneBufferGeometry(3, 3)
const groundMaterial = new THREE.MeshStandardMaterial({
color: 'rgb(20, 20, 30)',
roughness: 0.4,
metalness: 0.0,
side: THREE.DoubleSide
})
const ground = new THREE.Mesh(groundGeometry, groundMaterial)
ground.castShadow = ground.receiveShadow = true
ground.position.set(0, 0, 0)
ground.rotation.set(-Math.PI / 2, 0, 0)
scene.add(ground)
// light1
const light = new THREE.DirectionalLight(0xffffff)
light.position.set(-2, 2, 0.5)
light.castShadow = true
light.shadow.camera.top = 2
light.shadow.camera.bottom = -2
light.shadow.camera.right = 2
light.shadow.camera.left = -2
light.shadow.bias = -0.00001
light.shadow.mapSize.set(4096, 4096)
scene.add(light)
// light2
const hemiLight = new THREE.HemisphereLight(0xbbbbbb, 0x080808, 1)
scene.add(hemiLight)
// light1 helper
scene.add(new THREE.DirectionalLightHelper(light, 2, 0xFFFF00))
// axis helper
scene.add(new THREE.AxesHelper(100))
const depthMaterial = new THREE.RawShaderMaterial({
uniforms: {},
vertexShader: depthVertexShader,
fragmentShader: depthFragmentShader,
})
const depth = new THREE.WebGLRenderTarget(1, 1, {
wrapS: THREE.ClampToEdgeWrapping,
wrapT: THREE.ClampToEdgeWrapping,
minFilter: THREE.LinearFilter,
magFilter: THREE.LinearFilter,
format: THREE.RGBAFormat,
type: THREE.UnsignedByteType,
stencilBuffer: false,
depthBuffer: true
})
const hdr = new THREE.WebGLRenderTarget(1, 1, {
wrapS: THREE.ClampToEdgeWrapping,
wrapT: THREE.ClampToEdgeWrapping,
minFilter: THREE.LinearFilter,
magFilter: THREE.LinearFilter,
format: THREE.RGBAFormat,
type: THREE.UnsignedByteType,
stencilBuffer: false,
depthBuffer: true
})
// shield
const textureLoader = new THREE.TextureLoader()
const texture = textureLoader.load('./imgs/noise1.png')
texture.wrapS = texture.wrapT = THREE.RepeatWrapping
const shieldGeometry = new THREE.SphereBufferGeometry(0.5, 100, 100)
const shieldMaterial = new THREE.RawShaderMaterial({
uniforms: {
depthBuffer: { value: null },
resolution: { value: new THREE.Vector2(1, 1) },
bufColor: { value: null },
u_tex: { value: null },
time: { value: 0 }
},
vertexShader: shieldVertexShader,
fragmentShader: shieldFragmentShader,
transparent: true,
depthWrite: false,
side: THREE.DoubleSide
})
const shield = new THREE.Mesh(shieldGeometry, shieldMaterial)
shield.position.set(0, 0.3, 0)
shield.material.uniforms.depthBuffer.value = depth.texture
shield.material.uniforms.bufColor.value = depth.texture
shield.material.uniforms.u_tex.value = texture
scene.add(shield)
// tween
function moveCube() {
const tween = new TWEEN.Tween(cube.position)
tween.to({
x: 0.6,
z: 0.6
}, 5000)
tween.yoyo(true)
tween.repeat(Infinity)
tween.start()
}
// resize
function resize() {
const width = window.innerWidth
const height = window.innerHeight
const dPR = window.devicePixelRatio
camera.aspect = width / height
camera.updateProjectionMatrix()
renderer.setSize(width, height)
depth.setSize(width * dPR, height * dPR)
hdr.setSize(width * dPR, height * dPR)
shield.material.uniforms.resolution.value.set(width * dPR, height * dPR)
}
// render
function render() {
shield.visible = false
scene.overrideMaterial = depthMaterial
renderer.setRenderTarget(depth)
renderer.render(scene, camera)
shield.visible = true
scene.overrideMaterial = null
renderer.setRenderTarget(null)
renderer.render(scene, camera)
renderer.setAnimationLoop(render)
TWEEN.update()
stats.update()
controls.update()
shield.material.uniforms.time.value = performance.now()
}
window.addEventListener('resize', resize)
moveCube()
resize()
render()
</script>
</body>
</html>
depth-fs.js
const shader = `#version 300 es
precision highp float;
#include <packing>
in vec2 vUv;
in float vDepth;
out vec4 color;
void main() {
float depth = (vDepth - .1) / ( 10.0 -.1);
color = packDepthToRGBA(depth);
}
`;
export { shader };
depth-vs.js
const shader = `#version 300 es
precision highp float;
in vec3 position;
in vec2 uv;
uniform mat4 modelViewMatrix;
uniform mat4 projectionMatrix;
out vec2 vUv;
out float vDepth;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
vDepth = gl_Position.z;
}
`;
export { shader };
shield-fs.js
const shader = `#version 300 es
precision highp float;
#include <packing>
uniform sampler2D depthBuffer;
uniform vec2 resolution;
uniform float time;
uniform sampler2D u_tex;
in float vRim;
in vec2 vUv;
in float vDepth;
out vec4 color;
const vec4 baseColor = vec4(0.0,0.9,0.0,0.1);
void main() {
// 基础色
color = baseColor;
// 动态纹理
vec4 maskA = texture(u_tex, vUv);
maskA.a = maskA.r;
color += maskA;
// 边界高亮
vec2 uv = gl_FragCoord.xy / resolution;
vec4 packedDepth = texture(depthBuffer, uv);
float sceneDepth = unpackRGBAToDepth(packedDepth);
float depth = (vDepth - .1) / ( 10.0 -.1);
float diff = abs(depth - sceneDepth);
float contact = diff * 20.;
contact = 1. - contact;
contact = max(contact, 0.);
contact = pow(contact, 20.);
contact *= diff*1000.;
float a = max(contact, vRim);
float fade = 1. - pow(vRim, 10.);
color += a * fade;
}
`;
export { shader };
shield-vs.js
const shader = `#version 300 es
precision highp float;
in vec3 position;
in vec3 normal;
in vec2 uv;
uniform mat3 normalMatrix;
uniform mat4 modelMatrix;
uniform mat4 modelViewMatrix;
uniform mat4 projectionMatrix;
uniform float time;
out vec2 vUv;
out float vRim;
out float vDepth;
void main() {
vUv = uv;
vUv.x += time * 0.0001;
vUv.y += time * 0.0006;
vec3 n = normalMatrix * normal;
vec4 viewPosition = modelViewMatrix * vec4( position, 1. );
vec3 eye = normalize(-viewPosition.xyz);
vRim = 1.0 - abs(dot(eye,n));
vRim = pow(vRim, 5.);
vec3 worldPosition = (modelMatrix * vec4(position, 1.)).xyz;
gl_Position = projectionMatrix * viewPosition;
vDepth = gl_Position.z;
}
`;
export { shader };