<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>下“雪”了</title>
</head>
<body>
<script type="importmap">
{
"imports": {
"three": "https://unpkg.com/three@0.163.0/build/three.module.js",
"three/addons/": "https://unpkg.com/three@0.163.0/examples/jsm/"
}
}
</script>
<script type="module">
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import * as TWEEN from 'three/addons/libs/tween.module.js';
import { SimplexNoise } from 'three/addons/math/SimplexNoise.js';
let simplex = new SimplexNoise();
class TextTerrain extends THREE.Object3D {
constructor(anisotropy) {
super();
let alphabet = [
...'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz',
];
let textTexture = (() => {
let c = document.createElement('canvas');
let ctx = c.getContext('2d');
let texSize = 2048;
c.width = texSize;
c.height = texSize;
ctx.clearRect(0, 0, texSize, texSize);
let dim = 8;
let dimStep = texSize / dim;
for (let i = 0; i < alphabet.length; i++) {
let tileX = i % 8;
let tileY = Math.floor(i / 8);
let x = (tileX + 0.5) * dimStep;
let y = texSize - (tileY + 0.5) * dimStep;
ctx.fillStyle = `rgba(0, 0, 0, 1)`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.font = `bold ${dimStep * 0.9}px Arial`;
ctx.fillText(alphabet[i], x, y);
}
let tex = new THREE.CanvasTexture(c);
tex.colorSpace = 'srgb';
tex.anisotropy = anisotropy;
return tex;
})();
let tileDim = 200;
let g = new THREE.PlaneGeometry();
g.setAttribute(
'letterIdx',
new THREE.InstancedBufferAttribute(
new Float32Array(
Array.from(
{
length: tileDim * tileDim,
},
() => {
return THREE.MathUtils.randInt(0, alphabet.length - 1);
}
)
),
1
)
);
let m = new THREE.MeshBasicMaterial({
map: textTexture,
transparent: true,
alphaTest: 0.01,
side: THREE.DoubleSide,
onBeforeCompile: (shader) => {
shader.vertexShader = `
attribute float letterIdx;
varying float vLetterIdx;
${shader.vertexShader}
`.replace(
`#include <uv_vertex>`,
`#include <uv_vertex>
vLetterIdx = letterIdx;
`
);
shader.fragmentShader = `
varying float vLetterIdx;
${shader.fragmentShader}
`.replace(
`#include <map_fragment>`,
`
float letterIdx = floor(vLetterIdx + 0.1);
float tileStep = 1. / 8.;
float u = mod(letterIdx, 8.);
float v = floor(letterIdx / 8.);
vec2 iUv = (vec2(u, v) + vMapUv) * tileStep;
vec4 sampledDiffuseColor = texture2D( map, iUv );
diffuseColor *= sampledDiffuseColor;
`
);
},
});
let io = new THREE.InstancedMesh(g, m, tileDim * tileDim);
this.instancedMesh = io;
this.dummy = new THREE.Object3D();
this.finals = [];
let tri = new THREE.Triangle();
let n = new THREE.Vector3();
let la = new THREE.Vector3();
function getY(x, z) {
return simplex.noise(x * 0.01, z * 0.01) * 7.5;
}
let setFinals = () => {
let y0 = getY(this.dummy.position.x, this.dummy.position.z);
let y1 = getY(this.dummy.position.x, this.dummy.position.z - 1);
let y2 = getY(this.dummy.position.x + 1, this.dummy.position.z);
this.dummy.position.y = y0;
tri.a.set(this.dummy.position.x, y1, this.dummy.position.z - 1);
tri.b.set(this.dummy.position.x, y0, this.dummy.position.z);
tri.c.set(this.dummy.position.x + 1, y2, this.dummy.position.z);
tri.getNormal(n);
la.copy(this.dummy.position).add(n);
this.dummy.lookAt(la);
this.dummy.rotation.z = 0;
this.dummy.updateMatrix();
this.finals.push({
y: y0,
pos: this.dummy.position.clone(),
rot: this.dummy.rotation.clone(),
dummy: new THREE.Object3D(),
inAction: false,
mediators: {
v: new THREE.Vector3(),
v2: new THREE.Vector3(),
},
});
};
for (let z = 0; z < tileDim; z++) {
for (let x = 0; x < tileDim; x++) {
this.dummy.position.x = -(tileDim - 1) * 0.5 + x;
this.dummy.position.z = -(tileDim - 1) * 0.5 + z;
setFinals(this.dummy.position);
this.instancedMesh.setMatrixAt(
z * tileDim + x,
this.dummy.matrix
);
}
}
this.add(io);
this.actions = Array.from(
{
length: 5000,
},
() => {
let action = (delay) => {
let getFreeLetterIndex = () => {
let letterIndex = Math.floor(
Math.random() * this.finals.length
);
if (!this.finals[letterIndex].inAction) {
return letterIndex;
} else {
return getFreeLetterIndex();
}
};
let freeLetterIndex = getFreeLetterIndex();
let freeLetter = this.finals[freeLetterIndex];
let height = 30;
let m = freeLetter.mediators;
let v = m.v;
let v2 = m.v2;
v2.random()
.multiplyScalar(0.5)
.addScalar(0.5)
.multiplyScalar(Math.PI * 3 * Math.sign(Math.random() - 0.5));
let tween = new TWEEN.Tween({
val: 0,
})
.to(
{
val: 1,
},
10000
)
.delay(delay)
.onStart(() => {
freeLetter.inAction = true;
})
.onUpdate((val) => {
v.lerpVectors(v2, freeLetter.rot, val.val);
freeLetter.dummy.rotation.set(v.x, v.y, v.z);
freeLetter.dummy.position.copy(freeLetter.pos);
freeLetter.dummy.position.y = THREE.MathUtils.lerp(
height,
freeLetter.y,
val.val
);
freeLetter.dummy.updateMatrix();
io.setMatrixAt(freeLetterIndex, freeLetter.dummy.matrix);
})
.onComplete(() => {
freeLetter.inAction = false;
action(Math.random() * 10000);
});
tween.start();
};
return action;
}
);
}
}
let scene = new THREE.Scene();
scene.fog = new THREE.Fog('#fff', 100, 150);
scene.background = new THREE.Color('#fff');
let camera = new THREE.PerspectiveCamera(
45,
innerWidth / innerHeight,
1,
500
);
camera.position.set(0, 3, 8).setLength(50);
let renderer = new THREE.WebGLRenderer({
antialias: true,
});
renderer.setSize(innerWidth, innerHeight);
document.body.appendChild(renderer.domElement);
window.addEventListener('resize', (event) => {
camera.aspect = innerWidth / innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(innerWidth, innerHeight);
});
let controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
let textTerrain = new TextTerrain(
renderer.capabilities.getMaxAnisotropy()
);
scene.add(textTerrain);
textTerrain.actions.forEach((action) => {
action((Math.random() * 0.9 + 0.1) * 10000);
});
let clock = new THREE.Clock();
let t = 0;
renderer.setAnimationLoop(() => {
let dt = clock.getDelta();
t += dt;
TWEEN.update();
controls.update();
textTerrain.instancedMesh.instanceMatrix.needsUpdate = true;
renderer.render(scene, camera);
});
</script>
</body>
</html>