three.js - Animated Galaxy

  • 本篇文章将要绘制一个具有动画效果的galaxy,关于galaxy的实现可以参考之前的这篇笔记 Galaxy,我们在此基础上做了部分修改
  • We saw that animating each particles is a bad idea, animating the geometry buffer attribute on each frame is a bad idea, it must be have frame rate issue
  • so we're going to animate each particles by using vertex shader
  • Set up
<script setup>
  import * as THREE from 'three'
  import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
  import * as dat from 'dat.gui'

  /**
   * scene
  */
  const scene = new THREE.Scene()

  /**
   * Camera
  */
  const camera = new THREE.PerspectiveCamera(
    75,
    window.innerWidth / window.innerHeight,
    0.1,
    100
  )
  camera.position.set(3, 3, 3)

  /**
   * Renderer
  */
  const renderer = new THREE.WebGLRenderer()
  renderer.setSize(window.innerWidth, window.innerHeight)
  renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
  document.body.appendChild(renderer.domElement)

  window.addEventListener('resize', () => {
    camera.aspect = window.innerWidth / window.innerHeight
    camera.updateProjectionMatrix()

    renderer.setSize(window.innerWidth, window.innerHeight)
    renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
  })

  /**
   * control
  */
  const controls = new OrbitControls(camera, renderer.domElement)
  controls.enableDamping = true

  /**
   * render
  */
  const clock = new THREE.Clock()
  function tick () {
    let elapsedTime = clock.getElapsedTime()

    controls.update()
    requestAnimationFrame(tick)
    renderer.render(scene, camera)
  }
  tick()
</script>
  • 基于之前已实现的 Galaxy 修改后的 Galaxy,添加对应属性的gui
  /**
   * Galaxy
  */
  // 参数
  const parameters = {
    count: 200000,
    size: 0.005,
    radius: 5,  // 星系半径
    branches: 3,  // 星系分支,平分星系角度
    spin: 1,  // 旋转系数,geometry距离原点越远旋转角度越大
    randomness: 0.5,  // 随机性
    randomnessPower: 3,  // 随机性系数,可控制曲线变化
    insideColor: '#ff6030',
    outsideColor: '#1b3984',
  }

  let geometry = null
  let material = null
  let points = null

  const generateGalaxy = () => {
    if(points !== null) {
      geometry.dispose()
      material.dispose()
      scene.remove(points)
    }
    // geometry
    geometry = new THREE.BufferGeometry()

    const positions = new Float32Array(parameters.count * 3)
    const colors = new Float32Array(parameters.count * 3)

    const colorInside = new THREE.Color(parameters.insideColor)
    const colorOutside = new THREE.Color(parameters.outsideColor)

    for(let i = 0; i < parameters.count; i++) {
      const i3 = i * 3 

      // Position
      const radius = Math.random() * parameters.radius  
      const branchAngle = (i % parameters.branches) / parameters.branches * Math.PI * 2 

      const randomX = Math.pow(Math.random(), parameters.randomnessPower) * (Math.random() < 0.5 ? 1 : - 1) * parameters.randomness * radius
      const randomY = Math.pow(Math.random(), parameters.randomnessPower) * (Math.random() < 0.5 ? 1 : - 1) * parameters.randomness * radius
      const randomZ = Math.pow(Math.random(), parameters.randomnessPower) * (Math.random() < 0.5 ? 1 : - 1) * parameters.randomness * radius

      positions[i3] = Math.cos(branchAngle) * radius + randomX
      positions[i3 + 1] = randomY
      positions[i3 + 2] = Math.sin(branchAngle) * radius + randomZ

      // Color
      const mixedColor = colorInside.clone()
      mixedColor.lerp(colorOutside, radius / parameters.radius)

      colors[i3] = mixedColor.r
      colors[i3 + 1] = mixedColor.g
      colors[i3 + 2] = mixedColor.b
    }
    geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3))
    geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3))

    // material
    material = new THREE.PointsMaterial({
      size: parameters.size,
      sizeAttenuation: true,
      depthWrite: false,
      blending: THREE.AdditiveBlending,
      vertexColors: true,
    })

    // Points
    points = new THREE.Points(geometry, material) 
    scene.add(points)
  }
  generateGalaxy()
  /**
   * gui
  */
  const gui = new dat.GUI()
  gui.add(parameters, 'count')
    .min(100)
    .max(1000000)
    .step(100)
    .onFinishChange(generateGalaxy)
  gui.add(parameters, 'radius')
    .min(0.01)
    .max(20)
    .step(0.01)
    .onFinishChange(generateGalaxy)
  gui.add(parameters, 'branches')
    .min(2)
    .max(20)
    .step(1)
    .onFinishChange(generateGalaxy)
  gui.add(parameters, 'randomness')
    .min(0)
    .max(2)
    .step(0.001)
    .onFinishChange(generateGalaxy)
  gui.add(parameters, 'randomnessPower')
    .min(1)
    .max(10)
    .step(0.001)
    .onFinishChange(generateGalaxy)
  gui.addColor(parameters, 'insideColor').onFinishChange(generateGalaxy)
  gui.addColor(parameters, 'outsideColor').onFinishChange(generateGalaxy)
修改后的最初的galaxy.png
  • The first, we're going to create our own shader, we're going to replace PointsMaterial by ShaderMaterial
    • we get a warning telling us that the ShaderMaterial supports neither size nor sizeAttenuation, so remove these properties
    // material
    material = new THREE.ShaderMaterial({
      depthWrite: false,
      blending: THREE.AdditiveBlending,
      vertexColors: true,
    })
warning.png
ShaderMaterial.png
  • Shader
  import vertexShader from './animated-galaxy/vertex.glsl'
  import fragmentShader from './animated-galaxy/fragment.glsl'

    // material
    material = new THREE.ShaderMaterial({
      depthWrite: false,
      blending: THREE.AdditiveBlending,
      vertexColors: true,
      vertexShader,
      fragmentShader,
    })
// vertex.glsl
void main () {
  // Position
  vec4 modelPosition =  modelMatrix * vec4(position, 1.0);
  vec4 viewPosition = viewMatrix * modelPosition;
  vec4 projectedPosition = projectionMatrix * viewPosition;

  gl_Position = projectedPosition;

  // Size
  gl_PointSize = 2.0; // 内置变量,fragment size
}
// fragment.glsl
void main () {
  gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0);
}
shader.png
  • Handle the Size
    • because we're not use the PointsMaterial, we need to redo the color, the size, the size attenuation...
      // material
      material = new THREE.ShaderMaterial({
        ...
        uniforms: {
          uSize: {value: 8}
        }
      })
    
    // vertex.glsl
    uniform float uSize;
    
    void main () {
      // Position
      ...
    
      // Size
      gl_PointSize = uSize; // fragment size
    }
    
    • in real life, stars have different sizes, we can send a random value in the attributes
    const generateGalaxy = () => {
      ...
      ...
      const scales = new Float32Array(parameters.count)
    
      for(let i = 0; i < parameters.count; i++) {
          ...
          ...
          // Size
          scales[i] = Math.random()
      }
      ...
      geometry.setAttribute('aScale', new THREE.BufferAttribute(scales, 1))
    }
    
// vertex.glsl
uniform float uSize;

attribute float aScale;

void main () {
  // Position
  ...

  // Size
  gl_PointSize = uSize * aScale; // fragment size
}
Size.png
  • Fix the pixel ratio
    • the gl_PointSize means the fragment size(这里最好要理解下设备像素比的概念, 即物理像素/逻辑像素)
    • if you have a screen with a pixel ratio of 1, the particles will look 2 times larger than if the screen with a pixel ratio of 2
    • 浏览器获取设备像素比的方法是window.devicePixelRatio,但我们在构造render的部分已经设置过了renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
    // material
    material = new THREE.ShaderMaterial({
      ...
      uniforms: {
        uSize: {value: 8 * renderer.getPixelRatio()}
      }
    })

  /**
   * Renderer
  */
  ...

  window.addEventListener('resize', () => {
    ...
    renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
  })
  generateGalaxy()  // 将调用移动至renderer后面,防止上面在获取renderer变量时报错
  • Size Attenuation
    • 当我们只关注一颗particle时,会发现不管页面放大或是缩小,particle始终保持着相同大小
    • 我们当前使用的perspectiveCamera,呈现效果应当是particle距离camera越远,尺寸越小,这就是size attenuation
    • 原先使用PointsMaterial时,直接设置sizeAttenuation: true就可以了,但在shader中我们需要自己实现这一部分
    • we're going to take the code from the three.js depedencyPointsMaterial的顶点着色器文件路径: /node_modules/three/src/renderers/shaders/ShaderLib/points.glsl.js,我们需要关注的就是以下这段
      #ifdef USE_SIZEATTENUATION
          // 只有在perspectiveCamera下才会工作
          bool isPerspective = isPerspectiveMatrix( projectionMatrix );
    
          // scale is a value related to the renderer hight
          // particle的大小会根据屏幕的高度拉伸,这样不管是在大屏幕还是小屏幕,看到的particle都会差不多
    
          // mvPosition correspond to the position of the  vertex once the modelMatrix and the viewMatrix
          // in our case, it's viewPosition
          if ( isPerspective ) gl_PointSize *= ( scale / - mvPosition.z );
    
      #endif
    
    • 基于以上的代码来修改我们自己的,然后就会发现距离camare近的particle尺寸相对远的较大,这就是size attenuation,但我们在这部分并不想让particle随着屏幕大小的改变而拉伸
    // vertex.glsl
    ...
    void main () {
      ...
      ...
      // Size
      gl_PointSize = uSize * aScale; // fragment size
      gl_PointSize *= ( 1.0 / - viewPosition.z );
    }
    
Size attenuation.png
  • To draw our particle pattern
    • 通常是可以使用Meshuv属性来实现一些图案的,但在这里,我们使用的是new THREE.Points()Points对象是用于渲染大量的点的对象,他不具有uv属性,uv属性通常与Mesh一起使用,用于纹理映射
    • 在当前文件结构里,我们每一个vertex就代表一个particlefragment当然也一样,所以我们使用了内置变量gl_PointCoord
    • gl_PointCoord是一个二维向量,x 和 y 的取值范围都是[0, 1],表示当前片段在 Point Spirit 上的归一化坐标位置
    • 如下,在fragment中测试一下,看看得到的结果,同时再继续实现更多的形状
    // fragment.glsl
    void main () {
      gl_FragColor = vec4(gl_PointCoord, 1.0, 1.0);
    }
    
gl_PointCoord.png
  • Disc Pattern,边缘清晰的圆形
// fragment.glsl
void main () {
  // disc
  float strength = distance(gl_PointCoord, vec2(0.5));   // 计算当前片段与圆心(0.5, 0.5)的距离
  strength = step(0.5, strength);
  strength = 1.0 - strength;

  gl_FragColor = vec4(vec3(strength), 1.0);
}
Disc Pattern.png
  • Diffuse Point Pattern,边缘扩散的particle
// fragment.glsl
void main () {
  // diffuse point
  float strength = distance(gl_PointCoord, vec2(0.5));
  strength *= 2.0;
  strength = 1.0 - strength;

  gl_FragColor = vec4(vec3(strength), 1.0);
}
Diffuse Point Pattern.png
  • Light Point Pattern
// fragment.glsl
void main () {
  // light point
  float strength = distance(gl_PointCoord, vec2(0.5));
  strength = 1.0 - strength;
  strength = pow(strength, 10.0);

  gl_FragColor = vec4(vec3(strength), 1.0);
}
  material = new THREE.ShaderMaterial({
    ...
    ...
    uniforms: {
      uSize: {value: 30 * renderer.getPixelRatio()}
    }
  })
Light Point Pattern.png
  • Handle the Color
// vertex.glsl
...
varying vec3 vColor;

void main () {
  ...
  ...
  // Color
  vColor = color;
}
// fragmrnt.glsl
varying vec3 vColor;

void main () {
  // light point
  float strength = distance(gl_PointCoord, vec2(0.5));
  strength = 1.0 - strength;
  strength = pow(strength, 10.0);

  // Color
  vec3 color = mix(vec3(0.0), vColor, strength);  // 将黑色与color混合
  gl_FragColor = vec4(color, 1.0);
}
Handle the Color.png
  • Animate
    • we only need to rotate the particles on x and z
    • we calculate the particle angle and its distance to the center 我们计算particle和x轴之间的夹角以及particle到中心点的距离
    • we increase that angle according to the uTime and distance
    • we can use atan() to retrieve the angle 用于计算给定数值的反正切值
    // material
    material = new THREE.ShaderMaterial({
      ...
      uniforms: {
        uSize: {value: 30 * renderer.getPixelRatio()},
        uTime: {value: 0}
      }
    })
  /**
   * render
  */
  const clock = new THREE.Clock()
  function tick () {
    let elapsedTime = clock.getElapsedTime()

    // update material
    material.uniforms.uTime.value = elapsedTime
    ...
  }
  tick()
// vertex.glsl
uniform float uSize;
uniform float uTime;
...

void main () {
  // Position
  vec4 modelPosition =  modelMatrix * vec4(position, 1.0);

  float angle = atan(modelPosition.x, modelPosition.z);
  float distanceToCenter = length(modelPosition.xz);
  float angleOffset = (1.0 / distanceToCenter) * uTime * 0.2;
  angle += angleOffset;
  modelPosition.x = cos(angle) * distanceToCenter;
  modelPosition.z = sin(angle) * distanceToCenter;
  ...
  ...
}
Animate.png
  • 增加旋转后的随机性
    • 在创建galaxy时设置了一个参数randomness,只在设置初始position时使用了,旋转过程中却没有使用
    • 旋转过程中particle分布的随机性不好的话,就容易变得越来越线条,particle不能很好的分布在轴线附近,可以对比上下两张图,还是较为直观的
const generateGalaxy = () => {
  ...
  ...
  const randomness = new Float32Array(parameters.count * 3)
  ...
  for(let i = 0; i < parameters.count; i++) {
      ...
      // Position
      const radius = Math.random() * parameters.radius  
      const branchAngle = (i % parameters.branches) / parameters.branches * Math.PI * 2 

      positions[i3] = Math.cos(branchAngle) * radius
      positions[i3 + 1] = 0
      positions[i3 + 2] = Math.sin(branchAngle) * radius

      // random
      const randomX = Math.pow(Math.random(), parameters.randomnessPower) * (Math.random() < 0.5 ? 1 : - 1) * parameters.randomness * radius
      const randomY = Math.pow(Math.random(), parameters.randomnessPower) * (Math.random() < 0.5 ? 1 : - 1) * parameters.randomness * radius
      const randomZ = Math.pow(Math.random(), parameters.randomnessPower) * (Math.random() < 0.5 ? 1 : - 1) * parameters.randomness * radius

      randomness[i3] = randomX
      randomness[i3 + 1] = randomY
      randomness[i3 + 2] = randomZ
      ...
      ...
  } 
  ...
  geometry.setAttribute('aRandomness', new THREE.BufferAttribute(randomness, 3))
  ...
  ...
}
// vertex.glsl
...
attribute vec3 aRandomness;
...
void main () {
    // Position
    ...
    ...
    // random
    modelPosition.xyz += aRandomness;
    ...
    ...
}
randomness.png
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,039评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,223评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,916评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,009评论 1 291
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,030评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,011评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,934评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,754评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,202评论 1 309
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,433评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,590评论 1 346
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,321评论 5 342
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,917评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,568评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,738评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,583评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,482评论 2 352

推荐阅读更多精彩内容