文章目录
- 前言
- 三维心形函数
- 发光shader
- 使用对象键值对去重思路
- 最后
- 最后的最后——完整源码
- 相关项目
前言
最近很火的电视剧《点燃我,温暖你》男主角学神和女主角课代表计算机考试实现的跳动的爱心,那我也来做一个粒子爱心送给女朋友。因为不想直接加载心形的模型文件作为基础,所以主要思路是三维直角坐标系内,通过心形函数绘制图形。然后呢,细化到空间中坐标粒子的发光闪烁特效。实现思路讲完了,讲一下性能优化,因为通过函数找心形表面粒子的过程是需要遍历的(没办法令函数直接 = 0,只能取<0的数据点,取最大、最小值,再去重),多层遍历嵌套会降低页面加载速度,所以这里的思想是构建对象,因为去掉了一层循环,肯定是快很多的,只牺牲一点点内存构建对象。由于对象的键值对查找几乎不用时间,不受下标影响,就可以将性能发挥到极致。
三维心形函数
// Get surface points
// Calc xRange: [-1.12, 1.12]; yRange: [-0.96, 1.2]; zRange: [-0.64, 0.64]
// f() = (x^2 + 9/4 * z^2 + y^2 + 1)^3 - x^2 * y^3 - 9/80 * z^2 * y^3
const func = Math.pow(Math.pow(x, 2) + 9 / 4 * Math.pow(z, 2) + Math.pow(y, 2) - 1, 3) - Math.pow(x, 2) * Math.pow(y, 3) - 9 / 80 * Math.pow(z, 2) * Math.pow(y, 3)
if (func == 0) {
// you know
}
发光shader
<script type="x-shader/x-vertex" id="vertexshader">
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
</script>
<script type="x-shader/x-fragment" id="fragmentshader">
uniform sampler2D baseTexture;
uniform sampler2D bloomTexture;
varying vec2 vUv;
void main() {
gl_FragColor = (texture2D(baseTexture, vUv) + vec4(1.0) * texture2D(bloomTexture, vUv));
}
</script>
使用对象键值对去重思路
function objArrDistinct(objArr) {
let resultArr = [], itemKeyVal = {}
objArr.forEach(item => {
if (!itemKeyVal[`${item.x}_${item.y}_${item.z}`]) {
itemKeyVal[`${item.x}_${item.y}_${item.z}`] = true
resultArr.push(item)
}
})
return resultArr
}
最后
对比一下,谁的爱心更好看呢?(●ˇ∀ˇ●)
最后的最后——完整源码
<!DOCTYPE html>
<html lang="en">
<head>
<title>Heart</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
<link rel="shortcut icon" href="../../svg/heart.svg">
<link type="text/css" rel="stylesheet" href="../../main.css">
<style type="text/css"></style>
</head>
<body>
<script type="x-shader/x-vertex" id="vertexshader">
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
</script>
<script type="x-shader/x-fragment" id="fragmentshader">
uniform sampler2D baseTexture;
uniform sampler2D bloomTexture;
varying vec2 vUv;
void main() {
gl_FragColor = (texture2D(baseTexture, vUv) + vec4(1.0) * texture2D(bloomTexture, vUv));
}
</script>
<script type="module">
import * as THREE from '../../build/three.module.js'
import Stats from '../jsm/libs/stats.module.js'
import { OrbitControls } from '../jsm/controls/OrbitControls.js'
import { EffectComposer } from '../jsm/postprocessing/EffectComposer.js'
import { RenderPass } from '../jsm/postprocessing/RenderPass.js'
import { ShaderPass } from '../jsm/postprocessing/ShaderPass.js'
import { UnrealBloomPass } from '../jsm/postprocessing/UnrealBloomPass.js'
import { FXAAShader } from '../jsm/shaders/FXAAShader.js'
let renderer, scene, camera, controls, stats, pointLight
let transform = new THREE.Object3D()
let result = []
let heartMesh
let time = 0
let bloomComposer, finalComposer
const materials = {}
const ENTIRE_SCENE = 0
const BLOOM_SCENE = 1
const bloomLayer = new THREE.Layers()
bloomLayer.set(BLOOM_SCENE)
const darkMaterial = new THREE.MeshBasicMaterial({ color: 'black' })
initThree()
initBloom()
animate()
function randomSort(a, b) {
return Math.random() > 0.5 ? -1 : 1
}
function darkenNonBloomed(obj) {
if (obj instanceof THREE.Scene) {
materials.scene = obj.background
obj.background = null
return
}
if (obj instanceof THREE.Sprite || (obj.isMesh && bloomLayer.test(obj.layers) === false)) {
materials[obj.uuid] = obj.material
obj.material = darkMaterial
}
}
function restoreMaterial(obj) {
if (obj instanceof THREE.Scene) {
obj.background = materials.scene
delete materials.background
return
}
if (materials[obj.uuid]) {
obj.material = materials[obj.uuid]
delete materials[obj.uuid]
}
}
function initBloom() {
const effectFXAA = new ShaderPass(FXAAShader)
effectFXAA.uniforms['resolution'].value.set(0.6 / window.innerWidth, 0.6 / window.innerHeight)
effectFXAA.renderToScreen = true
const renderScene = new RenderPass(scene, camera)
const bloomPass = new UnrealBloomPass(new THREE.Vector2(window.innerWidth, window.innerHeight), 1.5, 0.4, 0.85)
bloomPass.threshold = 0
bloomPass.strength = 1.5
bloomPass.radius = 0
bloomComposer = new EffectComposer(renderer)
bloomComposer.renderToScreen = false
bloomComposer.addPass(renderScene)
bloomComposer.addPass(bloomPass)
bloomComposer.addPass(effectFXAA)
const finalPass = new ShaderPass(
new THREE.ShaderMaterial({
uniforms: {
baseTexture: { value: null },
bloomTexture: { value: bloomComposer.renderTarget2.texture },
},
vertexShader: document.getElementById('vertexshader').textContent,
fragmentShader: document.getElementById('fragmentshader').textContent,
defines: {},
}),
'baseTexture'
)
finalPass.needsSwap = true
finalComposer = new EffectComposer(renderer)
finalComposer.addPass(renderScene)
finalComposer.addPass(finalPass)
finalComposer.addPass(effectFXAA)
}
function initThree() {
stats = new Stats()
// document.body.appendChild(stats.domElement)
renderer = new THREE.WebGLRenderer({ antialias: false })
renderer.setPixelRatio(window.devicePixelRatio)
renderer.setSize(window.innerWidth, window.innerHeight)
document.body.appendChild(renderer.domElement)
scene = new THREE.Scene()
scene.background = new THREE.Color(0x111111)
camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 1, 10000)
camera.position.set(3, 3, 6)
camera.lookAt(0, 0, 0)
initOrbit()
initLight()
// initHelper()
initHeart()
toggleBloom()
window.addEventListener('resize', onWindowResize)
window.addEventListener('orientationchange', onWindowResize)
}
function toggleBloom() {
scene.traverse((obj) => {
if (obj.name === 'Heart') {
obj.layers.toggle(1)
}
})
}
function initOrbit() {
controls = new OrbitControls(camera, renderer.domElement)
controls.enablePan = false
controls.enableZoom = false
controls.enableDamping = true
controls.dampingFactor = 0.05
controls.target.set(0, 0, 0)
controls.autoRotate = true
}
function initLight() {
pointLight = new THREE.PointLight('#ffffff')
scene.add(pointLight)
}
function initHelper() {
const mainAxesHelper = new THREE.AxesHelper(10000)
scene.add(mainAxesHelper)
}
function initHeart() {
const arr_xyz = [] // All points
let arr_xy = [] // Points of x,y
let arr_yz = [] // Points of y,z
let arr_xz = [] // Points of x,z
const unitSize = 0.01 // unit size
const unitSpacing = 0.04 // unit spacing
// Get surface points
// Calc xRange: [-1.12, 1.12]; yRange: [-0.96, 1.2]; zRange: [-0.64, 0.64]
// let xmax = 0
// let xmin = 0
// let ymax = 0
// let ymin = 0
// let zmax = 0
// let zmin = 0
// arr_xyz.map(v => {
// xmax = Math.max(xmax, v.x)
// xmin = Math.min(xmin, v.x)
// ymax = Math.max(ymax, v.y)
// ymin = Math.min(ymin, v.y)
// zmax = Math.max(zmax, v.z)
// zmin = Math.min(zmin, v.z)
// })
// console.log(xmax, xmin, ymax, ymin, zmax, zmin)
let kvx = {}
let kvy = {}
let kvz = {}
for (let x = -1.12; x <= 1.12; x += unitSpacing) {
for (let y = -0.96; y <= 1.2; y += unitSpacing) {
for (let z = -0.64; z <= 0.64; z += unitSpacing) {
x = Number(x.toFixed(2))
y = Number(y.toFixed(2))
z = Number(z.toFixed(2))
// f() = (x^2 + 9/4 * z^2 + y^2 + 1)^3 - x^2 * y^3 - 9/80 * z^2 * y^3
const func = Math.pow(Math.pow(x, 2) + 9 / 4 * Math.pow(z, 2) + Math.pow(y, 2) - 1, 3) - Math.pow(x, 2) * Math.pow(y, 3) - 9 / 80 * Math.pow(z, 2) * Math.pow(y, 3)
if (func < 0) {
arr_xyz.push({ x, y, z })
kvx[`${y}_${z}`] = kvx[`${y}_${z}`] ? kvx[`${y}_${z}`].concat([x]) : [x]
kvy[`${x}_${z}`] = kvy[`${x}_${z}`] ? kvy[`${x}_${z}`].concat([y]) : [y]
kvz[`${x}_${y}`] = kvz[`${x}_${y}`] ? kvz[`${x}_${y}`].concat([z]) : [z]
arr_xy.push({ x, y, max_z: 0, min_z: 0 })
arr_yz.push({ z, y, max_x: 0, min_x: 0 })
arr_xz.push({ x, z, max_y: 0, min_y: 0 })
}
}
}
}
arr_xy = objArrDistinct(arr_xy)
arr_yz = objArrDistinct(arr_yz)
arr_xz = objArrDistinct(arr_xz)
// Filter out the maximum and minimum axial value
arr_xy.forEach(xy => {
xy.min_z = Math.min(...kvz[`${xy.x}_${xy.y}`])
xy.max_z = Math.max(...kvz[`${xy.x}_${xy.y}`])
})
arr_yz.forEach(yz => {
yz.min_x = Math.min(...kvx[`${yz.y}_${yz.z}`])
yz.max_x = Math.max(...kvx[`${yz.y}_${yz.z}`])
})
arr_xz.forEach(xz => {
xz.min_y = Math.min(...kvy[`${xz.x}_${xz.z}`])
xz.max_y = Math.max(...kvy[`${xz.x}_${xz.z}`])
})
// Filter out the surface points => result
arr_xy.map(xy => {
result.push({ x: xy.x, y: xy.y, z: xy.max_z })
result.push({ x: xy.x, y: xy.y, z: xy.min_z })
})
arr_yz.map(yz => {
result.push({ z: yz.z, y: yz.y, x: yz.max_x })
result.push({ z: yz.z, y: yz.y, x: yz.min_x })
})
arr_xz.map(xz => {
result.push({ x: xz.x, z: xz.z, z: xz.max_y })
result.push({ x: xz.x, z: xz.z, z: xz.min_y })
})
result = objArrDistinct(result)
// Set InstancedMesh
const geometry = new THREE.BoxBufferGeometry(unitSize, unitSize, unitSize)
const material = new THREE.MeshBasicMaterial()
heartMesh = new THREE.InstancedMesh(geometry, material, result.length)
heartMesh.name = 'Heart'
result = result.sort(randomSort)
result.map((res, i) => {
transform.position.set(res.x, res.y, res.z)
transform.updateMatrix()
heartMesh.setMatrixAt(i, transform.matrix)
heartMesh.setColorAt(i, new THREE.Color(`rgb(${Math.floor(255 * Math.random())}, 0, 0)`))
})
scene.add(heartMesh)
}
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight
camera.updateProjectionMatrix()
renderer.setSize(window.innerWidth, window.innerHeight)
}
/**
* Distinct 3d point (x, y, z)
*/
function objArrDistinct(objArr) {
let resultArr = [], itemKeyVal = {}
objArr.forEach(item => {
if (!itemKeyVal[`${item.x}_${item.y}_${item.z}`]) {
itemKeyVal[`${item.x}_${item.y}_${item.z}`] = true
resultArr.push(item)
}
})
return resultArr
}
function blingbling() {
const t = Date.now() * 0.005
result.map((res, i) => {
const scale = 1.2 * (0.5 * Math.sin(t + i * 0.1) + 0.5) // range of [0, 1.2]
transform.position.set(res.x, res.y, res.z)
transform.scale.set(scale, scale, scale)
transform.updateMatrix()
heartMesh.setMatrixAt(i, transform.matrix)
})
heartMesh.instanceMatrix.needsUpdate = true
}
function animate() {
blingbling()
stats.update()
controls.update()
scene.traverse(darkenNonBloomed)
bloomComposer.render()
scene.traverse(restoreMaterial)
finalComposer.render()
const vector = camera.position.clone()
pointLight.position.set(vector.x, vector.y, vector.z)
requestAnimationFrame(animate)
}
</script>
</body>
</html>
相关项目
🚩——坦克大战
📦—— 立体库房
🎄—— 圣诞树
✅—— 程序员升职记
🏀—— 投个篮吧
💖——粒子爱心