Skip to content

Commit 7770801

Browse files
authored
feat: add Stars
Adds a blinking shader-based starfield to your scene.
1 parent ad81c02 commit 7770801

File tree

4 files changed

+251
-2
lines changed

4 files changed

+251
-2
lines changed

.storybook/stories/Stars.stories.ts

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import * as THREE from 'three'
2+
import { Setup } from '../Setup'
3+
import GUI from 'lil-gui'
4+
import { Meta } from '@storybook/html'
5+
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'
6+
import { Stars, StarsProps } from '../../src/core/Stars'
7+
export default {
8+
title: 'Staging/Stars',
9+
} as Meta // TODO: this should be `satisfies Meta` but commit hooks lag behind TS
10+
11+
let gui: GUI
12+
let scene: THREE.Scene,
13+
camera: THREE.Camera,
14+
renderer: THREE.WebGLRenderer,
15+
animateLoop: (callback: (time: number) => void) => void
16+
17+
export const StarsStory = async () => {
18+
const setupResult = Setup()
19+
scene = setupResult.scene
20+
camera = setupResult.camera
21+
renderer = setupResult.renderer
22+
animateLoop = setupResult.render
23+
24+
gui = new GUI({ title: StarsStory.storyName })
25+
renderer.shadowMap.enabled = true
26+
renderer.toneMapping = THREE.ACESFilmicToneMapping
27+
camera.position.set(8, 5, 8)
28+
29+
const controls = new OrbitControls(camera, renderer.domElement)
30+
controls.target.set(0, 2, 0)
31+
controls.update()
32+
33+
scene.add(new THREE.AxesHelper())
34+
35+
scene.background = new THREE.Color(0x000000)
36+
gui.addColor(scene, 'background').name('Background Color')
37+
38+
setupStars()
39+
}
40+
41+
function setupStars() {
42+
const starParams: StarsProps = {
43+
radius: 100,
44+
depth: 50,
45+
count: 5000,
46+
saturation: 0,
47+
factor: 4,
48+
fade: false,
49+
speed: 1,
50+
}
51+
const stars = new Stars(starParams)
52+
53+
scene.add(stars)
54+
55+
const timer = new THREE.Timer()
56+
57+
// runs on every frame
58+
animateLoop(() => {
59+
timer.update()
60+
const elapsedTime = timer.getElapsed()
61+
stars.update(elapsedTime)
62+
})
63+
64+
// gui
65+
const updateStars = () => stars.rebuildAttributes(starParams)
66+
const folder = gui.addFolder('Stars')
67+
folder.add(starParams, 'radius', 1, 100, 1).onChange(updateStars)
68+
folder.add(starParams, 'depth', 1, 100, 1).onChange(updateStars)
69+
folder.add(starParams, 'count', 10, 10000, 10).onChange(updateStars)
70+
folder.add(starParams, 'saturation', 0, 1, 0.01).onChange(updateStars)
71+
folder.add(starParams, 'factor', 0.5, 10, 0.1).onChange(updateStars)
72+
folder.add(starParams, 'fade').onChange(updateStars)
73+
folder.add(starParams, 'speed', 0, 10, 0.1).onChange(updateStars)
74+
}
75+
76+
StarsStory.storyName = 'Stars'

README.md

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ import { pcss, ... } from '@pmndrs/vanilla'
5454
<li><a href="#cloud">Cloud</a></li>
5555
<li><a href="#camerashake">Camera Shake</a></li>
5656
<li><a href="#sparkles">Sparkles</a></li>
57+
<li><a href="#stars">Stars</a></li>
5758
</ul>
5859
<li><a href="#staging">Abstractions</a></li>
5960
<ul>
@@ -69,8 +70,8 @@ import { pcss, ... } from '@pmndrs/vanilla'
6970
<li><a href="#misc">Misc</a></li>
7071
<ul>
7172
<li><a href="#sprite-animator">Sprite Animator</a></li>
72-
</ul <li><a href="#portals">Portals</a></li>
73-
<ul>
73+
</ul <li><a href="#portals">Portals</a></li>
74+
<ul>
7475
<li><a href="#meshportalmaterial">MeshPortalMaterial</a></li>
7576
</ul>
7677
</ul>
@@ -569,6 +570,44 @@ function animate() {
569570

570571
```
571572
573+
#### Stars
574+
575+
[![storybook](https://img.shields.io/badge/-storybook-%23ff69b4)](https://pmndrs.github.io/drei-vanilla/?path=/story/staging-stars--stars-story)
576+
577+
Adds a blinking shader-based starfield to your scene.
578+
579+
```js
580+
export type StarsProps = {
581+
/** The radius of the starfield (default 100) */
582+
radius?: number
583+
/** The depth of the starfield (default 50) */
584+
depth?: number
585+
/** The number of stars (default 5000) */
586+
count?: number
587+
/** The factor by which to scale each star (default 4) */
588+
factor?: number
589+
/** The saturation of the stars (default 0) */
590+
saturation?: number
591+
/** Whether to fade the edge of each star (default false) */
592+
fade?: boolean
593+
/** The speed of the stars pulsing (default 1) */
594+
speed?: number
595+
}
596+
```
597+
598+
Usage
599+
600+
```js
601+
const stars = new Stars(starParams)
602+
scene.add(stars)
603+
604+
// in the update loop
605+
function animate() {
606+
stars.update(elapsedTime)
607+
...
608+
}
609+
```
610+
572611
#### Grid
573612
574613
[![storybook](https://img.shields.io/badge/-storybook-%23ff69b4)](https://pmndrs.github.io/drei-vanilla/?path=/story/gizmos-grid--grid-story)

src/core/Stars.ts

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
import * as THREE from 'three'
2+
import { version } from '../helpers/constants'
3+
4+
export type StarsProps = {
5+
/** The radius of the starfield (default 100) */
6+
radius?: number
7+
/** The depth of the starfield (default 50) */
8+
depth?: number
9+
/** The number of stars (default 5000) */
10+
count?: number
11+
/** The factor by which to scale each star (default 4) */
12+
factor?: number
13+
/** The saturation of the stars (default 0) */
14+
saturation?: number
15+
/** Whether to fade the edge of each star (default false) */
16+
fade?: boolean
17+
/** The speed of the stars pulsing (default 1) */
18+
speed?: number
19+
}
20+
21+
class StarfieldMaterial extends THREE.ShaderMaterial {
22+
constructor() {
23+
super({
24+
uniforms: { time: { value: 0.0 }, fade: { value: 1.0 } },
25+
vertexShader: /* glsl */ `
26+
uniform float time;
27+
attribute float size;
28+
varying vec3 vColor;
29+
void main() {
30+
vColor = color;
31+
vec4 mvPosition = modelViewMatrix * vec4(position, 0.5);
32+
gl_PointSize = size * (30.0 / -mvPosition.z) * (3.0 + sin(time + 100.0));
33+
gl_Position = projectionMatrix * mvPosition;
34+
}`,
35+
fragmentShader: /* glsl */ `
36+
uniform sampler2D pointTexture;
37+
uniform float fade;
38+
varying vec3 vColor;
39+
void main() {
40+
float opacity = 1.0;
41+
if (fade == 1.0) {
42+
float d = distance(gl_PointCoord, vec2(0.5, 0.5));
43+
opacity = 1.0 / (1.0 + exp(16.0 * (d - 0.25)));
44+
}
45+
gl_FragColor = vec4(vColor, opacity);
46+
47+
#include <tonemapping_fragment>
48+
#include <${version >= 154 ? 'colorspace_fragment' : 'encodings_fragment'}>
49+
}`,
50+
})
51+
}
52+
}
53+
54+
const genStar = (r: number) => {
55+
return new THREE.Vector3().setFromSpherical(
56+
new THREE.Spherical(r, Math.acos(1 - Math.random() * 2), Math.random() * 2 * Math.PI)
57+
)
58+
}
59+
60+
export class Stars extends THREE.Points {
61+
speed: number
62+
/**
63+
* Pulsing/Blinking shader-based starfield.
64+
*/
65+
constructor({
66+
radius = 100,
67+
depth = 50,
68+
count = 5000,
69+
saturation = 0,
70+
factor = 4,
71+
fade = false,
72+
speed = 1,
73+
}: StarsProps = {}) {
74+
super(new THREE.BufferGeometry(), new StarfieldMaterial())
75+
76+
this.speed = speed
77+
78+
const material = this.material as StarfieldMaterial
79+
material.blending = THREE.AdditiveBlending
80+
material.uniforms.fade.value = fade
81+
material.depthWrite = false
82+
material.transparent = true
83+
material.vertexColors = true
84+
material.needsUpdate = true
85+
86+
this.rebuildAttributes({ radius, depth, count, saturation, factor, fade })
87+
}
88+
89+
/**
90+
* Recreates the geometry buffers with the parameters.
91+
* Expensive operation - use for setup/configuration, not for animation.
92+
*/
93+
rebuildAttributes({
94+
radius = 100,
95+
depth = 50,
96+
count = 5000,
97+
saturation = 0,
98+
factor = 4,
99+
fade = false,
100+
speed = 1,
101+
}: StarsProps) {
102+
this.speed = speed
103+
const material = this.material as StarfieldMaterial
104+
material.uniforms.fade.value = fade
105+
106+
const positions: number[] = []
107+
const colors: number[] = []
108+
const sizes = Array.from({ length: count }, () => (0.5 + 0.5 * Math.random()) * factor)
109+
const color = new THREE.Color()
110+
let r = radius + depth
111+
const increment = depth / count
112+
for (let i = 0; i < count; i++) {
113+
r -= increment * Math.random()
114+
positions.push(...genStar(r).toArray())
115+
color.setHSL(i / count, saturation, 0.9)
116+
colors.push(color.r, color.g, color.b)
117+
}
118+
119+
this.geometry.setAttribute('position', new THREE.BufferAttribute(new Float32Array(positions), 3))
120+
this.geometry.setAttribute('color', new THREE.BufferAttribute(new Float32Array(colors), 3))
121+
this.geometry.setAttribute('size', new THREE.BufferAttribute(new Float32Array(sizes), 1))
122+
}
123+
124+
/**
125+
* Makes the stars pulse by updating its time uniform.
126+
* Call this in your animation loop.
127+
* @param elapsedTime Total elapsed time in seconds.
128+
*/
129+
update(elapsedTime: number): void {
130+
const material = this.material as StarfieldMaterial
131+
material.uniforms.time.value = elapsedTime * this.speed
132+
}
133+
}

src/core/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export * from './AccumulativeShadows'
88
export * from './Cloud'
99
export * from './CameraShake'
1010
export * from './Sparkles'
11+
export * from './Stars'
1112

1213
// Misc
1314
export * from './useFBO'

0 commit comments

Comments
 (0)