“使用 Three.js 的 WebGL 小实验。一个会冒泡的杯子。”
HTML:
<script type="importmap">
{
"imports": {
"three": "https://unpkg.com/three@0.162.0/build/three.module.js",
"three/addons/": "https://unpkg.com/three@0.162.0/examples/jsm/"
}
}
</script>
body{
overflow: hidden;
margin: 0;
}
JAVASCRIPT:
import * as THREE from "three";
import { OrbitControls } from "three/addons/controls/OrbitControls.js";
import { mergeGeometries, mergeVertices } from "three/addons/utils/BufferGeometryUtils.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(
"UnifrakturMaguntia",
"url(https://fonts.gstatic.com/s/unifrakturmaguntia/v20/WWXPlieVYwiGNomYU-ciRLRvEmK7oaVemGZM.woff2)"
)
];
for (let font in fonts) {
//console.log(fonts[font]);
await loadFont(fonts[font]);
}
})();
class Cup extends THREE.Mesh{
constructor(){
let profile = new THREE.Path()
.moveTo(0, 0.1)
.lineTo(4.4, 0.1)
.lineTo(4.4, 0.1)
.absarc(4.4, 0, 0.1, Math.PI * 0.5, 0, true)
.lineTo(4.5, 0)
.lineTo(4.75, 0)
.absarc(4.75, 0.25, 0.25, Math.PI * 0.25, 0)
.lineTo(5, 0.25)
.lineTo(5, 10)
.lineTo(5, 10)
.absarc(4.75, 10, 0.25, 0, Math.PI)
.lineTo(4.5, 10)
.lineTo(4.5, 0.7)
.lineTo(4.5, 0.7)
.absarc(4.4, 0.7, 0.1, 0, -Math.PI * 0.5, true)
.lineTo(4.4, 0.6)
.lineTo(0, 0.6)
.getPoints(200)
let gCup = new THREE.LatheGeometry(profile, 200);
// re-compute uvs
let pos = gCup.attributes.position;
let uvs = gCup.attributes.uv;
let nor = gCup.attributes.normal;
let n = new THREE.Vector3();
let p = new THREE.Vector3();
let v = new THREE.Vector2();
let t = new THREE.Vector2();
for(let i = 0; i < pos.count; i++){
p.fromBufferAttribute(pos, i);
n.fromBufferAttribute(nor, i);
v.set(p.x, p.z).normalize();
t.set(n.x, n.z).normalize();
if(v.dot(t) > 0.99){
let uvV = (p.y - 0.2) / 9.8;
let uvVClamp = THREE.MathUtils.clamp(uvV, 0, 1);
uvs.setY(i, uvV);
if (uvV < 0 || uvV > 1) uvs.setXY(i, 0, 0);
} else {
uvs.setXY(i, 0, 0);
}
}
gCup.rotateY(-Math.PI * 0.5);
// handle
let gHandle = new THREE.ExtrudeGeometry(
new THREE.Shape().absellipse(0, 0, 0.75, 0.3, 0, Math.PI * 2),
{
steps: 100,
bevelEnabled: true,
bevelSize: 0,
bevelThickness: 0.5,
bevelSegments: 10,
extrudePath: new THREE.CatmullRomCurve3([
[4.725, 8],
[5.5, 9],
[7, 9],
[8, 5],
[6, 3],
[4.75, 2]
].map(p => {return new THREE.Vector3(p[0], p[1], 0)}))
}
)
gHandle.deleteAttribute("uv");
gHandle.deleteAttribute("normal");
gHandle = mergeVertices(gHandle);
gHandle.computeVertexNormals();
gHandle.setAttribute("uv", new THREE.Float32BufferAttribute(new Array(gHandle.attributes.position.count * 2).fill(0), 2));
console.log(gCup, gHandle)
let g = mergeGeometries([gCup, gHandle]);
let m = new THREE.MeshPhysicalMaterial({
metalness: 0,
roughness: 1,
clearcoat: 0.5,
// wireframe: true
});
super(g, m);
let canvas = document.createElement("canvas");
canvas.width = 512;
canvas.height = 512;
canvas.unit = value => 0.01 * canvas.height * value;
this.ctx = canvas.getContext("2d");
this.texture = new THREE.CanvasTexture(canvas);
m.map = this.texture;
//m.map = new THREE.TextureLoader().load("https://threejs.org/examples/textures/uv_grid_opengl.jpg");
m.map.colorSpace = THREE.SRGBColorSpace;
m.map.wrapS = THREE.RepeatWrapping;
m.map.wrapT = THREE.RepeatWrapping;
m.map.repeat.set( 2, 1 );
this.bubbles = Array.from({length:20}, () => {
let pos = new THREE.Vector2().random().subScalar(0.5).multiplyScalar(100);
return {
position: new THREE.Vector3().copy(pos),
posInit: new THREE.Vector3().copy(pos),
size: Math.random() * 4 + 1,
speed: Math.random() * 10 + 10
}
})
//this.update(0);
// cider
let gCider = new THREE.LatheGeometry(
new THREE.Path()
.moveTo(0, 0)
.lineTo(4.4)
.absellipse(4.4, 0.05, 0.1, 0.05, -Math.PI * 0.5, 0)
.getPoints(50).reverse(),
200).translate(0, 8.5, 0);
let mCider = new THREE.MeshPhysicalMaterial({
color: "#da995f",
transparent: true,
metalness: 0,
roughness: 0,
ior: 1.25,
transmission: 1,
thickness: 0.05,
clearcoat: 1
});
let cider = new THREE.Mesh(gCider, mCider);
this.add(cider);
}
update(t){
let ctx = this.ctx;
let unit = ctx.canvas.unit;
ctx.fillStyle = "#006400";
ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);
ctx.save();
ctx.translate(unit(50), unit(50));
ctx.scale(1 / (Math.PI * 0.5), 1);
ctx.rotate(Math.PI / -6);
ctx.font = `${unit(50)}px UnifrakturMaguntia`;
ctx.textAlign = "center";
ctx.textBaseline = "middle";
let text = "Cidre";
ctx.fillStyle = "#f40";
ctx.fillText(text, unit(1), 0);
ctx.fillStyle = "#fff";
ctx.fillText(text, 0, 0);
ctx.restore();
ctx.save();
ctx.translate(unit(50), unit(50));
ctx.scale(1 / (Math.PI * 0.5), 1);
this.bubbles.forEach(bubble => {
let y = -50 + (bubble.posInit.y + t * bubble.speed + 50) % 100;
ctx.lineWidth = unit(1);
let a = THREE.MathUtils.smoothstep(y, -50, -30);
ctx.strokeStyle = `rgba(0, 159, 0, ${a})`;
ctx.beginPath();
ctx.arc(unit(bubble.position.x), unit( -y), unit(bubble.size), 0, Math.PI * 2);
ctx.stroke();
})
ctx.restore();
ctx.strokeStyle = "#7EA94C";
ctx.lineWidth = unit(2);
ctx.strokeRect(unit(0), unit(0), unit(100), unit(100));
this.texture.needsUpdate = true;
}
}
let scene = new THREE.Scene();
let camera = new THREE.PerspectiveCamera(30, innerWidth / innerHeight, 1, 100);
camera.position.set(0, 2, 8).setLength(30);
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 camShift = new THREE.Vector3(0, 5, 0);
camera.position.add(camShift);
let controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.target.copy(camShift);
const pmremGenerator = new THREE.PMREMGenerator( renderer );
scene.background = new THREE.Color( 0xFFE5B4 );
scene.environment = pmremGenerator.fromScene( new RoomEnvironment(), 0.04 ).texture;
let light = new THREE.DirectionalLight(0xffffff, Math.PI);
light.position.setScalar(1);
scene.add(light, new THREE.AmbientLight(0xffffff, Math.PI * 0.5));
let cup = new Cup();
scene.add(cup);
let clock = new THREE.Clock();
let t = 0;
renderer.setAnimationLoop(() => {
let dt = Math.min(clock.getDelta(), 1 / 60);
t += dt;
controls.update();
cup.update(t);
renderer.render(scene, camera);
});
源码:
https://codepen.io/prisoner849/pen/poBwLRG
体验:
https://codepen.io/prisoner849/full/poBwLRG