WebGL实现soul星球效果
点击关注公众号,“技术干货”及时达!
WebGL实现soul星球效果最近在研究webGL,觉得soul app的星球挺有意思的,于是就实现了一下,中间涉及的细节和知识点挺多的,写篇博客分享一下
soul原版WebGL实现的
主要技术要点1.自由转动因为要解决万向锁的问题,所以不能使用rotateX、rotateY、rotateZ来旋转,应当使用四元数THREE.Quaternion
2.背面小球变暗这里通过内部放置了一个半透明的黑色小球来实现
//创建半透明球体
constsphereGeometry=newTHREE.SphereGeometry(4.85,16,16)
为了使小球从正面转动的背面的过程中可以平滑的变暗,这里还需要把半透明小球的边沿处理成高斯模糊,具体实现就是使用GLSL的插值函数smoothstep
fragmentShader:`
uniformvec3color;
uniformfloatopacity;
varyingvec3vNormal;
voidmain(){
floatalpha=opacity*smoothstep(0.5,1.0,vNormal.z);
gl_FragColor=vec4(color,alpha);
}
但是需要注意的是需要关闭小球的深度测试,否则会遮挡小球
side:THREE.FrontSide,
depthWrite:false,
3.使用THREE.Sprite创建小球标签4.标签位置计算for(leti=0;inumPoints;i++){
consty=1-(i/(numPoints-1))*2
constradiusAtY=Math.sqrt(1-y*y)
consttheta=(2*Math.PI*i)/goldenRatio
constx=Math.cos(theta)*radiusAtY
constz=Math.sin(theta)*radiusAtY
constsmallBallMaterial=newTHREE.MeshBasicMaterial({
color:getRandomBrightColor(),
depthWrite:true,
depthTest:true,
side:THREE.FrontSide,
})
constsmallBall=newTHREE.Mesh(smallBallGeometry,smallBallMaterial)
smallBall.position.set(x*radius,y*radius,z*radius)
5.超出长度的标签采用贴图采样位移来实现跑马灯效果6.滚动阻尼,鼠标转动球体之后速度能衰减到转动旋转的速率7.自动旋转需要保持上一次滚动的方向8.使用射线拾取来实现点击交互完整代码!DOCTYPEhtml
htmllang="zh"
head
metacharset="UTF-8"/
title3D半透明球体与可交互小球/title
style
body{
margin:0;
background-color:black;
touch-action:none;
}
canvas{
display:block;
}
/style
/head
body
scripttype="module"
import*asTHREEfrom'https://cdn.jsdelivr.net/npm/three@0.169.0/build/three.module.js'
//创建场景
constscene=newTHREE.Scene()
//创建相机
constcamera=newTHREE.PerspectiveCamera(
75,
window.innerWidth/window.innerHeight,
0.1,
1000
)
camera.position.set(0,0,14)
camera.lookAt(0,0,0)
//创建渲染器
constrenderer=newTHREE.WebGLRenderer({antialias:true,alpha:true})
renderer.setSize(window.innerWidth,window.innerHeight)
renderer.setPixelRatio(window.devicePixelRatio)
renderer.setClearColor(0x000000,0)
document.body.appendChild(renderer.domElement)
//创建半透明球体
constsphereGeometry=newTHREE.SphereGeometry(4.85,16,16)
constsphereMaterial=newTHREE.ShaderMaterial({
uniforms:{
color:{value:newTHREE.Color(0x000000)},
opacity:{value:0.8},
},
vertexShader:`
varyingvec3vNormal;
voidmain(){
vNormal=normalize(normalMatrix*normal);
gl_Position=projectionMatrix*modelViewMatrix*vec4(position,1.0);
}
`,
fragmentShader:`
uniformvec3color;
uniformfloatopacity;
varyingvec3vNormal;
voidmain(){
floatalpha=opacity*smoothstep(0.5,1.0,vNormal.z);
gl_FragColor=vec4(color,alpha);
}
`,
transparent:true,
side:THREE.FrontSide,
depthWrite:false,
})
constsphere=newTHREE.Mesh(sphereGeometry,sphereMaterial)
scene.add(sphere)
//创建小球体和标签数组
constsmallBallGeometry=newTHREE.SphereGeometry(0.15,16,16)
constsmallBalls=[]
constlabelSprites=[]
constradius=5
constnumPoints=88
constgoldenRatio=(1+Math.sqrt(5))/2
constmaxWidth=160
consttextSpeed=0.002
//创建射线投射器
constraycaster=newTHREE.Raycaster()
constmouse=newTHREE.Vector2()
functioncreateTextTexture(text,parameters={}){
const{
fontSize=24,
fontFace='PingFangSC,MicrosoftYaHei,NotoSans,Arial,sans-serif',
textColor='white',
backgroundColor='rgba(0,0,0,0)',
maxWidth=160,
}=parameters
constcanvas=document.createElement('canvas')
constcontext=canvas.getContext('2d')
context.font=`${fontSize}px${fontFace}`
consttextMetrics=context.measureText(text)
consttextWidth=Math.ceil(textMetrics.width)
consttextHeight=fontSize*1.2
constneedMarquee=textWidthmaxWidth
letcanvasWidth=maxWidth
if(needMarquee){
canvasWidth=textWidth+60
}
canvas.width=canvasWidth
canvas.height=textHeight
context.font=`${fontSize}px${fontFace}`
context.clearRect(0,0,canvas.width,canvas.height)
context.fillStyle=backgroundColor
context.fillRect(0,0,canvas.width,canvas.height)
context.fillStyle=textColor
context.textAlign=needMarquee?'left':'center'
context.textBaseline='middle'
if(needMarquee){
context.fillText(text,0,canvas.height/2)
}else{
context.fillText(text,maxWidth/2,canvas.height/2)
}
consttexture=newTHREE.CanvasTexture(canvas)
texture.needsUpdate=true
if(needMarquee){
texture.wrapS=THREE.RepeatWrapping
texture.wrapT=THREE.ClampToEdgeWrapping
texture.repeat.x=maxWidth/canvas.width
}else{
texture.wrapS=THREE.ClampToEdgeWrapping
texture.wrapT=THREE.ClampToEdgeWrapping
}
texture.minFilter=THREE.LinearFilter
texture.magFilter=THREE.LinearFilter
texture.generateMipmaps=false
return{texture,needMarquee,HWRate:textHeight/maxWidth}
}
for(leti=0;inumPoints;i++){
consty=1-(i/(numPoints-1))*2
constradiusAtY=Math.sqrt(1-y*y)
consttheta=(2*Math.PI*i)/goldenRatio
constx=Math.cos(theta)*radiusAtY
constz=Math.sin(theta)*radiusAtY
constsmallBallMaterial=newTHREE.MeshBasicMaterial({
color:getRandomBrightColor(),
depthWrite:true,
depthTest:true,
side:THREE.FrontSide,
})
constsmallBall=newTHREE.Mesh(smallBallGeometry,smallBallMaterial)
smallBall.position.set(x*radius,y*radius,z*radius)
sphere.add(smallBall)
smallBalls.push(smallBall)
constlabelText=getRandomNickname()
const{texture,needMarquee,HWRate}=createTextTexture(labelText,{
fontSize:28,
fontFace:'PingFangSC,MicrosoftYaHei,NotoSans,Arial,sans-serif',
textColor:'#bbbbbb',
maxWidth:maxWidth,
})
constspriteMaterial=newTHREE.SpriteMaterial({
map:texture,
transparent:true,
depthWrite:true,
depthTest:true,
blending:THREE.NormalBlending,
})
constsprite=newTHREE.Sprite(spriteMaterial)
sprite.scale.set(1,HWRate,1)
labelSprites.push({sprite,smallBall,texture,needMarquee,labelText})
scene.add(sprite)
}
//添加灯光
constlight=newTHREE.AmbientLight(0xffffff,0.5)
scene.add(light)
constdirectionalLight=newTHREE.DirectionalLight(0xffffff,1)
directionalLight.position.set(5,5,5)
scene.add(directionalLight)
//定义自动旋转速度和轴
constautoRotationSpeed=0.0005
letautoRotationAxis=newTHREE.Vector3(0,1,0).normalize()
letcurrentAngularVelocity=autoRotationAxis.clone().multiplyScalar(autoRotationSpeed)
letisDragging=false
letpreviousMousePosition={x:0,y:0}
letlastDragDelta={x:0,y:0}
constdecayRate=0.92
constincreaseRate=1.02
//鼠标事件处理
constonMouseDown=(event)={
isDragging=true
previousMousePosition={
x:event.clientX,
y:event.clientY,
}
}
constonMouseMove=(event)={
if(isDragging){
constdeltaX=event.clientX-previousMousePosition.x
constdeltaY=event.clientY-previousMousePosition.y
lastDragDelta={x:deltaX,y:deltaY}
constrotationFactor=0.005
constangleY=deltaX*rotationFactor
constangleX=deltaY*rotationFactor
constquaternionY=newTHREE.Quaternion().setFromAxisAngle(
newTHREE.Vector3(0,1,0),
angleY
)
constquaternionX=newTHREE.Quaternion().setFromAxisAngle(
newTHREE.Vector3(1,0,0),
angleX
)
constdeltaQuat=newTHREE.Quaternion().multiplyQuaternions(quaternionY,quaternionX)
sphere.quaternion.multiplyQuaternions(deltaQuat,sphere.quaternion)
constdragRotationAxis=newTHREE.Vector3(deltaY,deltaX,0).normalize()
constdragRotationSpeed=Math.sqrt(deltaX*deltaX+deltaY*deltaY)*rotationFactor
if(dragRotationAxis.length()0){
currentAngularVelocity.copy(dragRotationAxis).multiplyScalar(dragRotationSpeed)
}
previousMousePosition={
x:event.clientX,
y:event.clientY,
}
}
}
constonMouseUp=()={
if(isDragging){
isDragging=false
constdeltaX=lastDragDelta.x
constdeltaY=lastDragDelta.y
if(deltaX!==0||deltaY!==0){
constnewAxis=newTHREE.Vector3(deltaY,deltaX,0).normalize()
if(newAxis.length()0){
autoRotationAxis.copy(newAxis)
}
constdragSpeed=currentAngularVelocity.length()
if(dragSpeedautoRotationSpeed){
//维持当前旋转速度
}else{
currentAngularVelocity.copy(autoRotationAxis).multiplyScalar(autoRotationSpeed)
}
}
}
}
//触摸事件处理
constonTouchStart=(event)={
isDragging=true
consttouch=event.touches[0]
previousMousePosition={
x:touch.clientX,
y:touch.clientY,
}
}
constonTouchMove=(event)={
event.preventDefault()
if(isDragging){
consttouch=event.touches[0]
constdeltaX=touch.clientX-previousMousePosition.x
constdeltaY=touch.clientY-previousMousePosition.y
lastDragDelta={x:deltaX,y:deltaY}
constrotationFactor=0.002
constangleY=deltaX*rotationFactor
constangleX=deltaY*rotationFactor
constquaternionY=newTHREE.Quaternion().setFromAxisAngle(
newTHREE.Vector3(0,1,0),
angleY
)
constquaternionX=newTHREE.Quaternion().setFromAxisAngle(
newTHREE.Vector3(1,0,0),
angleX
)
constdeltaQuat=newTHREE.Quaternion().multiplyQuaternions(quaternionY,quaternionX)
sphere.quaternion.multiplyQuaternions(deltaQuat,sphere.quaternion)
constdragRotationAxis=newTHREE.Vector3(deltaY,deltaX,0).normalize()
constdragRotationSpeed=Math.sqrt(deltaX*deltaX+deltaY*deltaY)*rotationFactor
if(dragRotationAxis.length()0){
currentAngularVelocity.copy(dragRotationAxis).multiplyScalar(dragRotationSpeed)
}
previousMousePosition={
x:touch.clientX,
y:touch.clientY,
}
}
}
constonTouchEnd=(event)={
if(isDragging){
isDragging=false
constdeltaX=lastDragDelta.x
constdeltaY=lastDragDelta.y
if(deltaX!==0||deltaY!==0){
constnewAxis=newTHREE.Vector3(deltaY,deltaX,0).normalize()
if(newAxis.length()0){
autoRotationAxis.copy(newAxis)
}
constdragSpeed=currentAngularVelocity.length()
if(dragSpeedautoRotationSpeed){
//维持当前旋转速度
}else{
currentAngularVelocity.copy(autoRotationAxis).multiplyScalar(autoRotationSpeed)
}
}
}
//检查点击事件
if(event.changedTouches.length0){
consttouch=event.changedTouches[0]
mouse.x=(touch.clientX/window.innerWidth)*2-1
mouse.y=-(touch.clientY/window.innerHeight)*2+1
checkIntersection()
}
}
//事件监听
window.addEventListener('mousedown',onMouseDown)
window.addEventListener('mousemove',onMouseMove)
window.addEventListener('mouseup',onMouseUp)
window.addEventListener('touchstart',onTouchStart)
window.addEventListener('touchmove',onTouchMove)
window.addEventListener('touchend',onTouchEnd)
document.addEventListener('gesturestart',function(e){
e.preventDefault()
})
//添加点击事件监听
window.addEventListener('click',onMouseClick)
//处理窗口大小调整
window.addEventListener('resize',()={
camera.aspect=window.innerWidth/window.innerHeight
camera.updateProjectionMatrix()
renderer.setSize(window.innerWidth,window.innerHeight)
})
functiononMouseClick(event){
event.preventDefault()
mouse.x=(event.clientX/window.innerWidth)*2-1
mouse.y=-(event.clientY/window.innerHeight)*2+1
console.log(event.clientX,mouse.x,mouse.y)
checkIntersection()
}
functioncheckIntersection(){
raycaster.setFromCamera(mouse,camera)
constintersects=raycaster.intersectObjects(smallBalls)
if(intersects.length0){
constintersectedBall=intersects[0].object
constindex=smallBalls.indexOf(intersectedBall)
if(index!==-1){
constlabelInfo=labelSprites[index]
showLabelInfo(labelInfo)
}
}
}
functionshowLabelInfo(labelInfo){
alert(`点击的小球标签:${labelInfo.labelText}`)
}
//动画循环
functionanimate(){
requestAnimationFrame(animate)
if(!isDragging){
constdeltaQuat=newTHREE.Quaternion().setFromEuler(
newTHREE.Euler(
currentAngularVelocity.x,
currentAngularVelocity.y,
currentAngularVelocity.z,
'XYZ'
)
)
sphere.quaternion.multiplyQuaternions(deltaQuat,sphere.quaternion)
constcurrentSpeed=currentAngularVelocity.length()
if(currentSpeedautoRotationSpeed){
currentAngularVelocity.multiplyScalar(decayRate)
if(currentAngularVelocity.length()autoRotationSpeed){
currentAngularVelocity.copy(autoRotationAxis).multiplyScalar(autoRotationSpeed)
}
}elseif(currentSpeedautoRotationSpeed){
currentAngularVelocity.multiplyScalar(increaseRate)
if(currentAngularVelocity.length()autoRotationSpeed){
currentAngularVelocity.copy(autoRotationAxis).multiplyScalar(autoRotationSpeed)
}
}else{
currentAngularVelocity.copy(autoRotationAxis).multiplyScalar(autoRotationSpeed)
}
}
//更新标签的位置和跑马灯效果
labelSprites.forEach(({sprite,smallBall,texture,needMarquee})={
smallBall.updateMatrixWorld()
constsmallBallWorldPos=newTHREE.Vector3()
smallBall.getWorldPosition(smallBallWorldPos)
constupOffset=newTHREE.Vector3(0,0.3,0)
sprite.position.copy(smallBallWorldPos).add(upOffset)
if(needMarquee){
texture.offset.x+=textSpeed
if(texture.offset.x1){
texture.offset.x=0
}
}
})
renderer.render(scene,camera)
}
animate()
functiongetRandomBrightColor(){
consthue=Math.floor(Math.random()*360)
constsaturation=Math.floor(Math.random()*40+10)
constlightness=Math.floor(Math.random()*40+40)
constrgb=hslToRgb(hue,saturation,lightness)
return(rgb.r16)|(rgb.g8)|rgb.b
}
functionhslToRgb(h,s,l){
s/=100
l/=100
constc=(1-Math.abs(2*l-1))*s
constx=c*(1-Math.abs(((h/60)%2)-1))
constm=l-c/2
letr,g,b
if(h=0h60){
r=c
g=x
b=0
}elseif(h=60h120){
r=x
g=c
b=0
}elseif(h=120h180){
r=0
g=c
b=x
}elseif(h=180h240){
r=0
g=x
b=c
}elseif(h=240h300){
r=x
g=0
b=c
}else{
r=c
g=0
b=x
}
return{
r:Math.round((r+m)*255),
g:Math.round((g+m)*255),
b:Math.round((b+m)*255),
}
}
functiongetRandomNickname(){
constadjectives=[
'Cool',
'Crazy',
'Mysterious',
'Happy',
'Silly',
'Brave',
'Smart',
'Swift',
'Fierce',
'Gentle',
]
constnouns=[
'Tiger',
'Lion',
'Dragon',
'Wizard',
'Ninja',
'Pirate',
'Hero',
'Ghost',
'Phantom',
'Knight',
]
constrandomAdjective=adjectives[Math.floor(Math.random()*adjectives.length)]
constrandomNoun=nouns[Math.floor(Math.random()*nouns.length)]
constnickname=`${randomAdjective}${randomNoun}`
if(nickname.length2){
returngetRandomNickname()
}elseif(nickname.length22){
returnnickname.slice(0,22)
}
returnnickname
}
/script
/body
/html
点击关注公众号,“技术干货”及时达!
阅读原文
网站开发网络凭借多年的网站建设经验,坚持以“帮助中小企业实现网络营销化”为宗旨,累计为4000多家客户提供品质建站服务,得到了客户的一致好评。如果您有网站建设、网站改版、域名注册、主机空间、手机网站建设、网站备案等方面的需求...
请立即点击咨询我们或拨打咨询热线:13245491521 13245491521 ,我们会详细为你一一解答你心中的疑难。 项目经理在线