“使用 Three.js 的 WebGL 小实验。JS 啤酒 - 万圣节版。”
HTML:
<iframe id="player" style="border: 0; width: 300px; height: 42px;" src="https://bandcamp.com/EmbeddedPlayer/album=996940111/size=small/bgcol=ffffff/linkcol=0687f5/track=2161815634/transparent=true/" seamless><a href="https://dbfiechter.bandcamp.com/album/night-at-the-carnival-ii">Night at the Carnival II by Derek & Brandon Fiechter</a></iframe>
<script type="importmap">
{
"imports": {
"three": "https://unpkg.com/three@0.169.0/build/three.webgpu.js",
"three/addons/": "https://unpkg.com/three@0.169.0/examples/jsm/"
}
}
</script>
<!--canvas id="cnv"></anvas-->
body{
overflow: hidden;
margin: 0;
}
#player {
/*display: none;*/
position: absolute;
bottom: 0;
margin: 10px;
border: 1px solid #080;
border-radius: 20px;
opacity: 0.1;
}
#player:hover{
opacity: 1;
}
JAVASCRIPT:
import * as THREE from "three";
import {
color,
pmremTexture,
mul, div, sub, add, floor,
mix, smoothstep, abs, max, step,
pow, clamp, normalize, negate,
oneMinus, dot
} from "three";
import { texture, uv, vec2, vec3, vec4, assign } from "three";
import { pass, mrt, output, bloom, emissive } from "three";
import {
Fn,
mx_noise_float,
mx_noise_vec3,
mx_fractal_noise_float
} from "three";
import {
normalWorld,
normalView,
positionView,
positionGeometry,
positionLocal,
normalGeometry,
timerLocal
} from "three";
import { OrbitControls } from "three/addons/controls/OrbitControls.js";
import { RGBELoader } from "three/addons/loaders/RGBELoader.js";
console.clear();
// load fonts
await (async function () {
async function loadFont(fontface) {
await fontface.load();
document.fonts.add(fontface);
}
let fonts = [
new FontFace(
"Sancreek",
"url(https://fonts.gstatic.com/s/sancreek/v25/pxiHypAnsdxUm159X4D5V14.woff2) format('woff2')"
)
];
for (let font in fonts) {
await loadFont(fonts[font]);
}
})();
class Postprocessing extends THREE.PostProcessing {
constructor(renderer) {
const scenePass = pass(scene, camera);
scenePass.setMRT(
mrt({
output,
emissive
})
);
const outputPass = scenePass.getTextureNode();
const emissivePass = scenePass.getTextureNode("emissive");
const bloomPass = bloom(emissivePass, 0.1, 0);
super(renderer);
this.outputNode = outputPass.add(bloomPass);
}
}
class Bottle extends THREE.Mesh {
constructor() {
let bottlePath = new THREE.Path()
.moveTo(0, 0)
.lineTo(2, 0)
.absarc(2, 0.5, 0.5, Math.PI * 1.5, 0)
.lineTo(2.5, 5)
.bezierCurveTo(2.5, 7.5, 0.5, 5.5, 0.5, 10)
.absarc(0.5, 10.25, 0.125, Math.PI * 1.5, Math.PI * 3)
.absarc(0, 6.5, 0.5, 0, Math.PI * 1.5, true);
let g = new THREE.LatheGeometry(bottlePath.getPoints(100), 144).rotateY(
Math.PI * -0.5
);
// re-compute uvs
let pos = g.attributes.position;
for (let i = 0; i < pos.count; i++) {
let y = pos.getY(i) - 0.5;
let v = y / 5;
g.attributes.uv.setY(i, v);
}
let beerJSTex = (() => {
let c = document.createElement("canvas");
c.width = c.height = 1024;
let u = (val) => c.height * 0.01 * val;
let ctx = c.getContext("2d");
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillStyle = "rgba(255, 255, 255, 1)";
ctx.font = `${u(15)}px Sancreek`;
ctx.fillText("JS", u(50), u(45));
ctx.font = `${u(30)}px Sancreek`;
ctx.fillText("BEER", u(50), u(75));
// eyes
let eye = (flip) => {
ctx.save();
ctx.translate(u(50), u(50));
ctx.scale(flip, 1);
ctx.translate(u(10), u(-22));
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.quadraticCurveTo(u(10), -u(5), u(25), u(-24));
ctx.quadraticCurveTo(u(25), u(25), 0, 0);
ctx.fill();
ctx.restore();
};
eye(1);
eye(-1);
let tex = new THREE.CanvasTexture(c);
tex.colorSpace = "srgb";
tex.anisotropy = 8;
return tex;
})();
let beerJSBackTex = (() => {
let c = document.createElement("canvas");
c.width = c.height = 1024;
let u = (val) => c.height * 0.01 * val;
let ctx = c.getContext("2d");
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillStyle = "rgba(255, 255, 255, 1)";
ctx.font = `${u(5)}px Sancreek`;
ctx.fillText("Shake thoroughly to blast!", u(50), u(10));
ctx.fillText("Ingredients:", u(50), u(25));
ctx.font = `${u(15)}px Sancreek`;
ctx.fillText("☢ ☣ ⚠", u(50), u(40));
ctx.fillText("♡ ☠", u(50), u(55));
ctx.font = `${u(5)}px Sancreek`;
ctx.fillText("♤ Drink at your own risk ♤", u(50), u(75));
ctx.font = `${u(3)}px Sancreek`;
ctx.fillText("BeerJS Brewery", u(50), u(90));
let tex = new THREE.CanvasTexture(c);
tex.colorSpace = "srgb";
tex.anisotropy = 8;
return tex;
})();
let m = new THREE.MeshStandardNodeMaterial({
roughness: 0.4,
metalness: 0.9
});
// TSL
let fillLevel = smoothstep(1.25, 1.75, uv().y).toVar();
m.colorNode = mix(color(0x00aa44), color(0x006600), fillLevel);
let texFront = texture(
beerJSTex,
uv().sub(vec2(0.25, 0.5)).mul(vec2(Math.PI, 1)).add(0.5)
).a.toVar();
let texBackUV = uv().sub(vec2(0.75, 0.5)).mul(vec2(Math.PI, 1)).add(0.5).toVar();
let texBack = texture(
beerJSBackTex,
texBackUV
).a.toVar();
let finalTexVal = max(texFront, texBack).toVar();
m.emissiveNode = finalTexVal.mul(color(0xff6600).mul(5));
/////
super(g, m);
// vapour
let vapourG = new THREE.LatheGeometry(
new THREE.Path()
.moveTo(2.25, 5.5)
.splineThru([
[2, 6.5],
[1.5, 8],
[1, 10],
[0, 11]
].map(p => new THREE.Vector2(...p)))
.getSpacedPoints(100),
72
).rotateY(Math.PI);
let vapourM = new THREE.MeshBasicNodeMaterial({
//wireframe: true,
side: THREE.DoubleSide,
depthWrite: false,
transparent: true
});
// TSL
let timer = timerLocal();
vapourM.positionNode = Fn(() => {
let posNoise = positionGeometry.add(vec3(0, timer, 0)).mul(0.5).toVar();
let noise = mx_noise_float(posNoise).mul(0.5).add(0.5);
return positionGeometry.add(normalGeometry.mul(noise).mul(0.75));
})();
vapourM.colorNode = Fn(() => {
let alpha = smoothstep(0, 0.9, uv().y)
.pow(2)
.toVar();
let colorNoiseFade = smoothstep(0, 0.9, uv().y).toVar();
let colorNoise = mx_fractal_noise_float(
vec3(
uv()
.mul(vec2(5, 1.5))
.add(vec2(0, timer.mul(0.5))),
timer.mul(0.1)
)
)
.abs()
.oneMinus()
.pow(8)
.mul(colorNoiseFade)
.toVar();
alpha.assign(clamp(alpha.add(colorNoise), 0, 1).mul(smoothstep(0.8, 1, uv().y).oneMinus()));
let smoothing = smoothstep(0., 1, dot(abs(normalView), normalize(positionView).negate())).toVar();
return vec4(color(0x008800), alpha.mul(0.75).mul(smoothing));
})();
/////
let vapour = new THREE.Mesh(vapourG, vapourM);
this.add(vapour);
}
}
let scene = new THREE.Scene();
scene.backgroundNode = color(0x000000);
let camera = new THREE.PerspectiveCamera(30, innerWidth / innerHeight, 1, 1000);
camera.position.set(0, 0.35, 1).setLength(22.5);
let renderer = new THREE.WebGPURenderer({ antialias: true });
renderer.setPixelRatio(devicePixelRatio);
renderer.setSize(innerWidth, innerHeight);
renderer.toneMapping = THREE.ACESFilmicToneMapping;
document.body.appendChild(renderer.domElement);
let postprocessing = new Postprocessing(renderer);
window.addEventListener("resize", (event) => {
camera.aspect = innerWidth / innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(innerWidth, innerHeight);
});
let camShift = new THREE.Vector3(0, 5.25, 0);
camera.position.add(camShift);
let controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.autoRotate = true;
controls.autoRotateSpeed *= -1;
controls.minPolarAngle = Math.PI * 0.25;
controls.maxPolarAngle = Math.PI * 0.75;
controls.target.copy(camShift);
let bgTexture = new RGBELoader()
.setPath("https://threejs.org/examples/textures/equirectangular/")
.load("royal_esplanade_1k.hdr", function (tex) {
tex.mapping = THREE.EquirectangularReflectionMapping;
scene.environment = tex;
scene.backgroundNode = pmremTexture(tex, normalWorld, 0.5).mul(0.1);
});
let light = new THREE.AmbientLight(0xffffff, Math.PI);
//scene.add(light);
let bottle = new Bottle();
bottle.rotation.y = Math.PI * 0.25;
scene.add(bottle);
renderer.setAnimationLoop(() => {
controls.update();
postprocessing.render();
});
源码:
https://codepen.io/prisoner849/pen/gOVvxeX
体验:
https://codepen.io/prisoner849/full/gOVvxeX