Snow scene with particles and Frieren

You can visit and play around the parameters here:
snow-scene-particles.vercel.app

The piling snow is a shapekey/morph controlled by the sliders, and the snow falling animation is a custom shader!

Snow scene with particles❄ and Frieren

Play around the parameters: https://t.co/9xYYegzLGS #threeJs pic.twitter.com/AspVMdbpKM

— Shikaki (@z_shikaki) December 23, 2024
13 Likes

Up til now, I wasn’t even aware of “Frieren” being part of a universal language. Sort of like “Kindergarten” or “Schadenfreude” …

3 Likes

Good outline!

1 Like

oh no haha don’t worry it’s just the character’s name

Thank you! There is probably a better way to do it since I’m exporting double the amount of geometry from blender, but the scene is light enough that it doesn’t matter

1 Like

I’m not worried. “Frieren” is a regular word in the German language and denotes the turning of water into ice: “Wasser ge-frier-t zu Eis”. And it also denotes when a person (or animal, for that matter) is apparently feeling cold. So it’s actually a very fitting name for the character.

3 Likes

Ohh I see. I knew the names in the show had some sort of meaning, after looking it up apparently a lot of the other character’s names are based on German words, that’s interesting to know, thanks!

2 Likes

I would use noise for the snow movement, now I can clearly see a sine wave, is too predictable.

looks super nice! good work!
is this available on github or codesandbox?

I kinda like the uniformness of it but true it does look off especially when you turn up the sliders, a noise texture would be a good idea

Thank you! No, it’s not really available. But if I can copy paste stuff if you need anything specific from it. It’s a bit messy though

I’ve no issue with messy code. You can publish the whole thing if you want as I don’t mind going through the code.

Suree, here’s the code

import * as THREE from 'three';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import snowvertex from './snow/vertex.glsl'
import snowfragment from './snow/fragment.glsl'
import {Pane} from 'tweakpane';

//loaders and debug items
const loader = new GLTFLoader();
const textureLoader = new THREE.TextureLoader();
const pane = new Pane({title: 'Parameters',});

const f1 = pane.addFolder({title: 'May Affect Frieren',});
const f2 = pane.addFolder({title: 'Wont Affect Frieren',});


const debugObject = {
	timespeed:1.0,
	background: "rgb(127, 177, 184)",
}

//sizes
const sizes = {
    width: window.innerWidth,
    height: window.innerHeight
}
window.addEventListener('resize', () =>
	{
		// Update sizes
		sizes.width = window.innerWidth
		sizes.height = window.innerHeight
	
		// Update camera
		camera.aspect = sizes.width / sizes.height
		camera.updateProjectionMatrix()
	
		// Update renderer
		renderer.setSize(sizes.width, sizes.height)
		renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
		snowMaterial.uniforms.uPixelRatio.value = renderer.getPixelRatio()
	})

//scene camera render
const scene = new THREE.Scene();
scene.background = new THREE.Color( debugObject.background )
f2.addBinding(debugObject, 'background',{label:'Color'}).on
('change', () => {scene.background.setStyle(debugObject.background)});

const camera = new THREE.PerspectiveCamera( 75, sizes.width / sizes.height, 0.01, 50 );
const renderer = new THREE.WebGLRenderer({antialias:true});
renderer.setSize( sizes.width, sizes.height );
document.body.appendChild( renderer.domElement );


//controler
const controls = new OrbitControls( camera, renderer.domElement );
camera.position.set( 1, 1, 3 );
controls.update();
controls.maxDistance = 30;


//materials
const stafftext = textureLoader.load("snowstaff.webp", t => {t.flipY = false ; t.colorSpace = THREE.SRGBColorSpace})
const frierentext = textureLoader.load("snowfrieren.webp", t => {t.flipY = false ; t.colorSpace = THREE.SRGBColorSpace})
const frierenEYEtext = textureLoader.load("snowfrieren.webp", t => {t.flipY = false ; t.colorSpace = THREE.SRGBColorSpace})
const frierenMOUTHtext = textureLoader.load("snowfrieren.webp", t => {t.flipY = false ; t.colorSpace = THREE.SRGBColorSpace})

const staffmat = new THREE.MeshBasicMaterial( {map:stafftext} );
const frierenmat = new THREE.MeshBasicMaterial( {map:frierentext} );
const frierenEYEmat = new THREE.MeshBasicMaterial( {map:frierenEYEtext} );
const frierenMOUTHmat = new THREE.MeshBasicMaterial( {map:frierenMOUTHtext} );
const vertexcolors = new THREE.MeshBasicMaterial( {vertexColors:true} );
const linemat = new THREE.MeshBasicMaterial( {color:'rgb(50, 0, 50)'} );

const snowtex = textureLoader.load('particle.png');

const snowMaterial = new THREE.ShaderMaterial({
	uniforms:
	{
		uTime: {value:0},
		uPixelRatio:{value: renderer.getPixelRatio()},
		uSize: {value:15},
		uRadius: {value:0.5},
		uTexture: {value: snowtex},
		uSimple:{value: true},
	},
	vertexShader:snowvertex,
	fragmentShader:snowfragment,
	transparent:true,
	depthWrite:false
});
f1.addBinding(snowMaterial.uniforms.uSize,'value',{min:1,max:50,step:1,label:'Size'}).on
('change', () => {snowbuildup()});
f2.addBinding(snowMaterial.uniforms.uRadius,'value',{min:0.01,max:3,step:0.01,label:'Wind'})
f2.addBinding(snowMaterial.uniforms.uSimple,'value',{label:'Simple'})


//loading scene
let movingsnow;
let movingsnowline;
loader.load('frieren snow scene.glb',
	function ( gltf ) {
		scene.add(gltf.scene);
		gltf.scene.getObjectByName('bggeo').material = vertexcolors
		gltf.scene.getObjectByName('bggeo_1').material = linemat
		gltf.scene.getObjectByName('cubegeo').material = vertexcolors
		gltf.scene.getObjectByName('cubegeo_1').material = linemat
		gltf.scene.getObjectByName('staffgeo').material = staffmat
		gltf.scene.getObjectByName('staffgeo_1').material = linemat

		movingsnow = gltf.scene.getObjectByName('cubegeo');
		movingsnowline = gltf.scene.getObjectByName('cubegeo_1');
	})


//loading frieren
let mixer
let sittingAnim
let sittingColdAnim
loader.load('frieren.glb',
	function ( gltf ) {
		gltf.scene.getObjectByName("Cube017").material = frierenmat
		gltf.scene.getObjectByName("Cube017_1").material = frierenEYEmat
		gltf.scene.getObjectByName("Cube017_2").material = frierenMOUTHmat
		gltf.scene.getObjectByName("Cube017_3").material = linemat

		mixer = new THREE.AnimationMixer(gltf.scene)
		
		sittingAnim = mixer.clipAction(gltf.animations[0])
		sittingColdAnim = mixer.clipAction(gltf.animations[1])
		sittingAnim.play()
		
		scene.add(gltf.scene);
	})


//snow particles
const snowGeometry = new THREE.BufferGeometry()
debugObject.numParticles = 1500
let positionArray
function generatesnow(n) {
	positionArray = new Float32Array(n*3)
	for( let i = 0; i < n; i++ ) {
		positionArray[i*3+0] = rand(7)
		positionArray[i*3+1] = Math.random() * 7
		positionArray[i*3+2] = rand(7)
	}
	snowGeometry.setAttribute('position', new THREE.BufferAttribute(positionArray,3))
}
generatesnow(debugObject.numParticles);
f1.addBinding(debugObject,'numParticles',{min:1,max:20000,step:1,label:'Amount', index: 0},).on
('change', () => {
	generatesnow(debugObject.numParticles)
	snowbuildup()
});

const snow = new THREE.Points(snowGeometry,snowMaterial)
scene.add(snow)

// update
const clock = new THREE.Clock()
renderer.setAnimationLoop(update);
f2.addBinding(debugObject,'timespeed',{min:0.1,max:10,step:0.1,label:'Speed',index: 2})


function update() {
	const DELTA = clock.getDelta()
	const ELAPSED = clock.getElapsedTime()
	snowMaterial.uniforms.uTime.value = ELAPSED * debugObject.timespeed
	if ( mixer ) mixer.update(DELTA);

	renderer.render( scene, camera );
}

//helper functions

function rand( v ) {return (v * (Math.random() - 0.5)); }

let issnowbuildup = false
function snowbuildup(){
	movingsnow.morphTargetInfluences[0] = movingsnowline.morphTargetInfluences[0] =
	(Math.min(Math.max(debugObject.numParticles *snowMaterial.uniforms.uSize.value, 20000), 300000)-20000) / 280000
	if (issnowbuildup) {
		if (movingsnow.morphTargetInfluences[0] < 0.3) {
			sittingColdAnim.stop()
			sittingAnim.play()
			mixer.setTime(mixer.time)
			frierenEYEtext.offset.set(0,0)
			frierenMOUTHtext.offset.set(0,0)
			issnowbuildup = false
		}
	}
	else {
		if (movingsnow.morphTargetInfluences[0] > 0.3) {
			sittingAnim.stop()
			sittingColdAnim.play()
			mixer.setTime(mixer.time)
			frierenEYEtext.offset.set(0,0.250)
			frierenMOUTHtext.offset.set(0,0.3125)
			issnowbuildup = true
		}
	}

}

thanks but it seems the glsl files are missing

vertex shader

uniform float uTime;
uniform float uPixelRatio;
uniform float uSize;
uniform float uRadius;

void main()
{
    vec4 modelPosition = modelMatrix * vec4(position, 1.0) ;
    modelPosition.y =  mod(modelPosition.y - uTime *0.2,6.0);

    modelPosition.z = mod(modelPosition.z + (sin((uTime + modelPosition.x )*0.5))* uRadius * 0.8,7.0)-3.5;
    modelPosition.x = mod(modelPosition.x + (cos((uTime + modelPosition.z )*0.5))* uRadius,7.0)-3.5;

    vec4 viewPosition = viewMatrix * modelPosition ;
    vec4 projectionPosition = projectionMatrix * viewPosition ;

    gl_Position = projectionPosition ;
    gl_PointSize = uSize * uPixelRatio * ( 1.0/ -viewPosition.z);

}

fragment shader

uniform sampler2D uTexture;
uniform bool uSimple;

void main() {
    float distanceToCenter = distance(gl_PointCoord, vec2(0.5));
    float strength = 0.5/distanceToCenter - 1.0;
    vec4 texture = texture2D(uTexture,gl_PointCoord);

    if (uSimple) 
        {gl_FragColor = vec4 ( 1.0,1.0,1.0,strength );}
        else {gl_FragColor = texture;}
    
}

optional particle texture
particle