“使用 Three.js 的 WebGL 小实验。非常酷的效果,数字时钟。”
HTML:
type="importmap">
{
{ :
"https://unpkg.com/three@0.162.0/build/three.module.js", :
"https://unpkg.com/three@0.162.0/examples/jsm/" :
}
}
</script>
<script>
let snoise2d = `// Simplex 2D noise
//
vec3 permute(vec3 x) { return mod(((x*34.0)+1.0)*x, 289.0); }
float snoise(vec2 v){
const vec4 C = vec4(0.211324865405187, 0.366025403784439,
0.024390243902439);
vec2 i = floor(v + dot(v, C.yy) );
vec2 x0 = v - i + dot(i, C.xx);
vec2 i1;
i1 = (x0.x > x0.y) ? vec2(1.0, 0.0) : vec2(0.0, 1.0);
vec4 x12 = x0.xyxy + C.xxzz;
-= i1;
i = mod(i, 289.0);
vec3 p = permute( permute( i.y + vec3(0.0, i1.y, 1.0 ))
i.x + vec3(0.0, i1.x, 1.0 ));
vec3 m = max(0.5 - vec3(dot(x0,x0), dot(x12.xy,x12.xy),
0.0);
m = m*m ;
m = m*m ;
vec3 x = 2.0 * fract(p * C.www) - 1.0;
vec3 h = abs(x) - 0.5;
vec3 ox = floor(x + 0.5);
vec3 a0 = x - ox;
m *= 1.79284291400159 - 0.85373472095314 * ( a0*a0 + h*h );
vec3 g;
a0.x * x0.x + h.x * x0.y; =
a0.yz * x12.xz + h.yz * x12.yw; =
return 130.0 * dot(m, g);
}
float udSegment( in vec2 p, in vec2 a, in vec2 b )
{
vec2 ba = b-a;
vec2 pa = p-a;
float h =clamp( dot(pa,ba)/dot(ba,ba), 0.0, 1.0 );
return length(pa-h*ba);
}
`;
</script>
body{
overflow: hidden;
margin: 0;
}
JAVASCRIPT:
import * as THREE from "three";
import { OrbitControls } from "three/addons/controls/OrbitControls.js";
import { mergeGeometries } from "three/addons/utils/BufferGeometryUtils.js";
import { EffectComposer } from "three/addons/postprocessing/EffectComposer.js";
import { RenderPass } from "three/addons/postprocessing/RenderPass.js";
import { UnrealBloomPass } from "three/addons/postprocessing/UnrealBloomPass.js";
import { OutputPass } from "three/addons/postprocessing/OutputPass.js";
import { RoomEnvironment } from "three/addons/environments/RoomEnvironment.js";
console.clear();
// load fonts
await (async function () {
async function loadFont(fontface) {
await fontface.load();
document.fonts.add(fontface);
}
let fonts = [
new FontFace(
"KodeMono",
"url(https://fonts.gstatic.com/s/kodemono/v1/A2BYn5pb0QgtVEPFnlYOnYLw.woff2) format('woff2')"
)
];
for (let font in fonts) {
//console.log(fonts[font]);
await loadFont(fonts[font]);
}
})();
class GlowLayer extends EffectComposer {
constructor(renderer) {
const target = new THREE.WebGLRenderTarget(
window.innerWidth,
window.innerHeight,
{
type: THREE.HalfFloatType,
format: THREE.RGBAFormat,
colorSpace: THREE.SRGBColorSpace,
samples: 8
}
);
super(renderer, target);
const renderScene = new RenderPass(scene, camera);
const bloomPass = new UnrealBloomPass(
new THREE.Vector2(window.innerWidth, window.innerHeight),
0.1,
0,
5
);
const outputPass = new OutputPass();
this.uniforms = {
aspect: { value: camera.aspect }
};
outputPass.material.onBeforeCompile = (shader) => {
shader.uniforms.aspect = this.uniforms.aspect;
shader.uniforms.watermark = {value: (function () {
let c = document.createElement("canvas");
c.width = 1024;
c.height = 128;
let $ = c.getContext("2d");
$.clearRect(0, 0, c.width, c.height);
$.font = `bold ${c.height * 0.5}px KodeMono`;
$.textAlign = "left";
$.textBaseline = "middle";
$.fillStyle = "#fff";
$.fillText("849 # Die Digitaluhr", c.width * 0.01, c.height * 0.5);
let cTex = new THREE.CanvasTexture(c);
cTex.colorSpace = THREE.SRGBColorSpace;
return cTex;
})()
}
shader.fragmentShader = `
${shader.fragmentShader}
`
.replace(
`precision highp float;`,
`precision highp float;
uniform float aspect;
uniform sampler2D watermark;
`
)
.replace(
`}`,
`
// signature
vec2 sUv = vUv * 14. * vec2(aspect / 8., 1.);
sUv -= vec2(0.05, 0.05);
float sig = texture2D(watermark, sUv).r;
vec3 sigColor = vec3(0.75);
////////////
gl_FragColor.rgb = mix(gl_FragColor.rgb, sigColor, sig);
}
`
);
//console.log(shader.fragmentShader)
};
this.addPass(renderScene);
this.addPass(bloomPass);
this.addPass(outputPass);
//console.log(outputPass);
}
}
class Logo extends THREE.BufferGeometry {
constructor() {
super();
let baseVector = new THREE.Vector2(0, 1);
let center = new THREE.Vector2();
let shift = new THREE.Vector2(0, -0.25);
let a = 3 / Math.sqrt(3);
let hA = a * 0.5;
let hStep = 1.5;
let hHeight = 0.75;
let steps = 4;
let scale = 0.85;
let baseTri = [
baseVector.clone().multiplyScalar(scale).add(shift),
baseVector
.clone()
.rotateAround(center, (-Math.PI * 2) / 3)
.multiplyScalar(scale)
.add(shift),
baseVector
.clone()
.rotateAround(center, (Math.PI * 2) / 3)
.multiplyScalar(scale)
.add(shift)
];
let baseTriFlip = [
baseVector
.clone()
.rotateAround(center, Math.PI)
.multiplyScalar(scale)
.sub(shift),
baseVector
.clone()
.rotateAround(center, Math.PI / 3)
.multiplyScalar(scale)
.sub(shift),
baseVector
.clone()
.rotateAround(center, -Math.PI / 3)
.multiplyScalar(scale)
.sub(shift)
];
let holes = [];
for (let rows = 0; rows < steps; rows++) {
let items = 1 + rows * 2; // arithmetic progression
let h = hStep * 1.5 - rows * hStep;
console.log(h);
let w = -((items - 1) / 2) * hA;
for (let item = 0; item < items; item++) {
let shiftX = w + hA * item;
let shiftY = h;
let tri = (item % 2 == 0 ? baseTri : baseTriFlip).map((p) => {
let pt = p.clone();
pt.x += shiftX;
pt.y += shiftY;
return pt;
});
let hole = new THREE.Path(tri);
holes.push(hole);
}
}
let contourShift = new THREE.Vector2(0, -1);
let contour = [
baseVector.clone().multiplyScalar(4.1).add(contourShift),
baseVector
.clone()
.rotateAround(center, (Math.PI * 2) / 3)
.multiplyScalar(4.1)
.add(contourShift),
baseVector
.clone()
.rotateAround(center, (-Math.PI * 2) / 3)
.multiplyScalar(4.1)
.add(contourShift)
];
let shape = new THREE.Shape(contour);
shape.holes = holes;
let shapeGeom = new THREE.ExtrudeGeometry(shape, {
depth: 0.1,
bevelEnabled: true,
bevelThickness: 0.1,
bevelSize: 0.1,
bevelSegments: 5
});
shapeGeom.rotateZ(Math.PI * 0.25);
shapeGeom.center();
this.copy(shapeGeom);
}
}
class Display extends THREE.InstancedMesh {
constructor(thickness) {
let g = new THREE.PlaneGeometry(5, 7.5);
let m = new THREE.MeshBasicMaterial({
color: new THREE.Color(0x0088ff),
map: null,
side: THREE.DoubleSide,
forceSinglePass: true,
transparent: true
});
super(g, m, 10);
this.thickness = thickness;
console.log(this);
this.init();
}
init() {
let dummy = new THREE.Object3D();
for (let i = 0; i < this.count; i++) {
dummy.position.z = i * (this.thickness / (this.count - 1));
dummy.updateMatrix();
this.setMatrixAt(i, dummy.matrix);
}
let canvas = document.createElement("canvas");
canvas.width = 1000;
canvas.height = 1400;
this.context = canvas.getContext("2d");
this.days = [
"SONNTAG",
"MONTAG",
"DIENSTAG",
"MITTWOCH",
"DONNERSTAG",
"FREITAG",
"SAMSTAG"
];
this.displayTexture = new THREE.CanvasTexture(canvas);
this.material.map = this.displayTexture;
this.material.onBeforeCompile = (shader) => {
shader.vertexShader = `
varying float viID;
${shader.vertexShader}
`.replace(
`#include <begin_vertex>`,
`#include <begin_vertex>
float iID = float(gl_InstanceID);
viID = iID;
`
);
//console.log(shader.vertexShader);
shader.fragmentShader = `
varying float viID;
${shader.fragmentShader}
`.replace(
`vec4 diffuseColor = vec4( diffuse, opacity );`,
`vec4 diffuseColor = vec4( diffuse, opacity );
float aa = viID / ${this.count - 1}.;
aa *= aa * aa * aa;
diffuseColor.a = aa;
float iID = floor(viID + 0.1);
diffuseColor.rgb *= iID == ${this.count - 1}. ? 20. : 1.;
if ((!gl_FrontFacing) && (iID < ${this.count - 1}.)) discard;
`
);
//console.log(shader.fragmentShader);
};
//this.material.needsUpdate = true;
}
update() {
let date = new Date();
//console.log(date);
let ctx = this.context;
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
ctx.fillStyle = "rgba(255, 255, 255, 1)";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
let DoW = this.days[date.getDay()];
ctx.font = "bold 75px KodeMono";
ctx.fillText(DoW, ctx.canvas.width * 0.5, ctx.canvas.height * 0.3);
let days = ("0" + date.getDate()).slice(-2);
let month = ("0" + (date.getMonth() + 1)).slice(-2);
let year = date.getFullYear();
let fullDate = days + "." + month + "." + year;
ctx.font = "bold 100px KodeMono";
ctx.fillText(fullDate, ctx.canvas.width * 0.5, ctx.canvas.height * 0.4);
let hours = ("0" + date.getHours()).slice(-2);
let minutes = ("0" + date.getMinutes()).slice(-2);
let milliseconds = date.getMilliseconds();
let time = hours + (milliseconds < 500 ? ":" : " ") + minutes;
ctx.font = "bold 300px KodeMono";
ctx.fillText(time, ctx.canvas.width * 0.5, ctx.canvas.height * 0.6);
/*
ctx.strokeStyle = "rgba(255, 255, 255, 1)";
ctx.lineWidth = 5;
ctx.strokeRect(0, 0, ctx.canvas.width, ctx.canvas.height);
*/
this.displayTexture.needsUpdate = true;
}
}
class Digitaluhr extends THREE.Mesh {
constructor() {
let axis = new THREE.Vector3(0, 0, 1);
let baseContour = [
[0.1, 2],
[1, 0],
[0, -1],
[-1, 0],
[-0.1, 2]
].map((p) => {
return new THREE.Vector3(p[0], p[1], 0).multiplyScalar(5);
//.applyAxisAngle(axis, Math.PI * 0.25);
});
let thickness = 1.25;
// extrusion
let shapeCurve = new THREE.CatmullRomCurve3([...baseContour], true);
let holeCurve = new THREE.CatmullRomCurve3(
[...baseContour].map((p) => {
return p.clone().multiplyScalar(0.5);
}),
true
);
let shape = new THREE.Shape(shapeCurve.getSpacedPoints(1000));
shape.holes.push(new THREE.Path(holeCurve.getSpacedPoints(1000).reverse()));
let gExtrude = new THREE.ExtrudeGeometry(shape, {
depth: thickness,
steps: 1,
bevelEnabled: false
});
let mExtrude = new THREE.MeshLambertMaterial({
color: new THREE.Color().setHSL(0.33, 1, 1)
});
let goldMaterial = new THREE.MeshStandardMaterial({
color: 0xff0000,
metalness: 1,
roughness: 0.25
});
super(gExtrude, [mExtrude, goldMaterial]);
this.position.y = 5.2;
this.position.z = -thickness * 0.5;
//this.rotation.x = Math.PI * -0.5;
this.castShadow = true;
this.receiveShadow = true;
let gOutline = mergeGeometries([
new THREE.TubeGeometry(shapeCurve, 1000, 0.2, 16, true),
new THREE.TubeGeometry(shapeCurve, 1000, 0.2, 16, true).translate(
0,
0,
thickness
),
new THREE.TubeGeometry(holeCurve, 1000, 0.2, 16, true),
new THREE.TubeGeometry(holeCurve, 1000, 0.2, 16, true).translate(
0,
0,
thickness
)
]);
let outline = new THREE.Mesh(gOutline, goldMaterial);
outline.castShadow = true;
outline.receiveShadow = true;
this.add(outline);
let logo = new THREE.Mesh(new Logo(), goldMaterial);
logo.scale.setScalar(0.4);
logo.position.set(0, 7, thickness);
logo.castShadow = true;
this.add(logo);
this.display = new Display(thickness);
this.display.position.y = 1.125;
this.add(this.display);
//display.update();
}
}
class Background extends THREE.Mesh {
constructor(backcolor) {
let g = new THREE.SphereGeometry(900, 64, 32);
let m = new THREE.MeshBasicMaterial({
color: backcolor,
side: THREE.BackSide
});
m.defines = { USE_UV: "" };
super(g, m);
this.uniforms = {
time: { value: 0 }
};
this.material.onBeforeCompile = (shader) => {
shader.uniforms.time = this.uniforms.time;
shader.fragmentShader = `
uniform float time;
${snoise2d}
${shader.fragmentShader}
`.replace(
`vec4 diffuseColor = vec4( diffuse, opacity );`,
`
float t = time * 0.1;
vec2 uv = vUv;
uv -= 0.5;
vec2 uvMult = vec2(40., 4.);
uv *= uvMult;
uv.y += 0.33;
uv.y = abs(uv.y);
vec2 cId = floor(uv);
vec2 cUv = fract(uv);
cUv.x -= 0.5;
cUv.x /= 5.;
float rnd = snoise(cId + vec2(0., t)) * 0.5 + 0.5;
float baseVal = 0.1;
float baseRand = baseVal + rnd * 0.5;
float rounding = 0.1;
vec2 a = vec2(0., baseVal);
vec2 b = vec2(0., baseRand);
float f = udSegment(cUv, a, b) - rounding;
vec2 fw = fwidth(cUv);
float mf = min(fw.x, fw.y);
float df = smoothstep(mf, -mf, f);
float circf = length(vec2(0., baseRand + 0.3) - cUv) - rounding * 0.5;
float circ = smoothstep(mf, -mf, circf);
df = max(df, circ);
df *= step(0., cId.y) - step(1., cId.y); // first row only
vec3 col = mix(diffuse, diffuse + 0.005, df);
vec4 diffuseColor = vec4( col, opacity );
`
);
//console.log(shader.fragmentShader)
};
}
update(t) {
this.uniforms.time.value = t;
}
}
let scene = new THREE.Scene();
scene.background = new THREE.Color(0x111111);
let camera = new THREE.PerspectiveCamera(45, innerWidth / innerHeight, 1, 1000);
camera.position.set(-5, 2, 8).setLength(20);
let renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(innerWidth, innerHeight);
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
document.body.appendChild(renderer.domElement);
let cameraShift = new THREE.Vector3(0, 8, 0);
let glowLayer = new GlowLayer(renderer);
window.addEventListener("resize", (event) => {
camera.aspect = innerWidth / innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(innerWidth, innerHeight);
glowLayer.setSize(innerWidth, innerHeight);
glowLayer.uniforms.aspect.value = camera.aspect;
});
let controls = new OrbitControls(camera, renderer.domElement);
controls.enablePan = false;
controls.enableDamping = true;
controls.minDistance = 10;
controls.maxDistance = 25;
controls.maxPolarAngle = Math.PI * 0.5;
controls.target.copy(cameraShift);
controls.object.position.add(cameraShift);
controls.update();
const pmremGenerator = new THREE.PMREMGenerator(renderer);
scene.environment = pmremGenerator.fromScene(
new RoomEnvironment(),
0.04
).texture;
let light = new THREE.DirectionalLight(0xffffff, Math.PI);
light.castShadow = true;
light.shadow.mapSize.width = 2048;
light.shadow.mapSize.height = 2048;
light.shadow.camera.near = 0.5;
light.shadow.camera.far = 100;
let hSize = 7;
light.shadow.camera.top = hSize;
light.shadow.camera.bottom = -hSize;
light.shadow.camera.left = -hSize;
light.shadow.camera.right = hSize;
light.position.setScalar(20).add(cameraShift);
let lightTarget = new THREE.Object3D();
scene.add(lightTarget);
lightTarget.position.copy(cameraShift);
light.target = lightTarget;
scene.add(light, new THREE.AmbientLight(0xffffff, 0.5));
//let grid = new THREE.GridHelper(10, 10);
//grid.rotation.x = Math.PI * 0.5;
//scene.add(grid);
let digitaluhr = new Digitaluhr();
scene.add(digitaluhr);
let surface = new THREE.Mesh(
new THREE.CircleGeometry(20, 72).rotateX(-Math.PI * 0.5),
new THREE.MeshLambertMaterial({
color: 0x404040,
transparent: true,
onBeforeCompile: (shader) => {
shader.uniforms.outerShade = { value: scene.background };
shader.fragmentShader = `
uniform vec3 outerShade;
${shader.fragmentShader}
`.replace(
`#include <dithering_fragment>`,
`#include <dithering_fragment>
float os = smoothstep(0.3, 0.5, length(vUv - 0.5));
gl_FragColor.a = 1. - os; //mix(gl_FragColor.rgb, outerShade, os);
`
);
//console.log(shader.fragmentShader);
}
})
);
surface.material.defines = { USE_UV: "" };
surface.receiveShadow = true;
scene.add(surface);
let background = new Background(scene.background);
scene.add(background);
let clock = new THREE.Clock();
let t = 0;
renderer.setAnimationLoop(() => {
let dt = clock.getDelta();
t += dt;
controls.update();
digitaluhr.display.update();
background.update(t);
glowLayer.render();
//renderer.render(scene, camera);
});
源码:
https://codepen.io/prisoner849/pen/wvZGjBj
体验:
https://codepen.io/prisoner849/full/wvZGjBj