aboutsummaryrefslogtreecommitdiffhomepage
path: root/public/bluebey-studio/src/styles.js
diff options
context:
space:
mode:
authorYasutake Yohei <61961825+yasutakeyohei@users.noreply.github.com>2026-10-07 23:09:40 +0900
committerYasutake Yohei <61961825+yasutakeyohei@users.noreply.github.com>2026-10-07 23:09:40 +0900
commitbc2821adb11a30244fc4663f4fafb756877d9508 (patch)
treefea8d30d2eb1e67f895097dbf7ec4d152361243e /public/bluebey-studio/src/styles.js
parent0d50c5ede0812ba7b67b775c9cd0bf52d7b65e02 (diff)
bluebey-studio: public/ の外へ移動し非公開化
Diffstat (limited to 'public/bluebey-studio/src/styles.js')
-rw-r--r--public/bluebey-studio/src/styles.js457
1 files changed, 0 insertions, 457 deletions
diff --git a/public/bluebey-studio/src/styles.js b/public/bluebey-studio/src/styles.js
deleted file mode 100644
index 2e4f62d..0000000
--- a/public/bluebey-studio/src/styles.js
+++ /dev/null
@@ -1,457 +0,0 @@
-import * as THREE from 'three';
-
-/**
- * Render styles for the body meshes, plus the outline pass.
- *
- * real the PBR materials exactly as authored in the GLB
- * flat two-tone toon shading, the classic mascot look
- * lineart white paper + lines, the face artwork drawn as strokes
- * outline nothing but the lines, kept invisible via colorWrite = false so the
- * result composites on top of anything (used by the SVG export)
- *
- * The lines come from two systems, each used where it is strong (see
- * MODEL-GUIDE.md §5): every mesh gets an inverted-hull copy, and the thin
- * overlapping leaves are handed to the screen-space pass instead (outline.js),
- * which the caller arranges with `setHullHidden`.
- */
-
-export const STYLE_DEFS = [
- { value: 'real', label: 'リアル' },
- { value: 'flat', label: 'フラット' },
- { value: 'lineart', label: '線画' },
-];
-
-// 'outline' is still used internally by the SVG export (lines on a transparent
-// background), it is just no longer offered as a viewing style.
-const LINE_STYLES = new Set(['lineart', 'outline']);
-
-export const isLineStyle = (value) => LINE_STYLES.has(value);
-
-/** Parts that need no outline at all. */
-const OUTLINE_SKIP = new Set();
-
-/**
- * Parts whose outline is only useful in the line-art styles.
- *
- * Empty now: the leaves used to be listed here, but the line styles get their
- * leaf lines from the `Vein` faces themselves (see OUTLINE_SHADED_ONLY), so the
- * hull is the wrong tool for them in either direction.
- */
-const OUTLINE_LINE_ONLY = new Set();
-
-/**
- * Parts that should be outlined in the SHADED styles only.
- *
- * The inverted hull draws an outline by expanding a copy of the mesh along its
- * normals and rendering the back faces. That works on one big round body, but
- * the leaf skirt is a dozen thin shells lying on top of each other: each hull's
- * far side shows through its neighbours, which is what produced the faint
- * overlapping hairlines. In the SVG pass the leaves get their line from their
- * own `Vein` faces (filled with the ink colour) instead, so the hull is dropped
- * there and kept for the cartoon styles, where it does read as an edge.
- */
-const OUTLINE_SHADED_ONLY = new Set(['vein']);
-
-/**
- * Per-part outline thickness, as a fraction of the global width.
- *
- * The hull is offset by a fixed distance in world units, so the right factor
- * depends on how fine a part's mesh is compared with that offset:
- *
- * - the nose is only ~0.24 units across, and the offset that looks right on the
- * body reads as a thick ring on it, hence the small factor. In the line-art
- * styles it is the other way round (a hairline looks like dust on paper), so
- * the nose gets a fuller line there - see OUTLINE_SCALE_LINE.
- * - the leaves need a *larger* factor, which only shows up in the "hull only"
- * style: the 13 blades overlap and interpenetrate, so each one's expanded
- * shell cuts across its neighbours and the line breaks up. A thicker hull
- * merges those scratches back into a band. Making the leaves thicker does NOT
- * fix it - measured, see MODEL-GUIDE.md §5-4 - because the overlaps are the
- * blocker, not the leaf's own thickness.
- */
-const OUTLINE_SCALE = { nose: 0.4, leaf: 1.7, vein: 2.2 };
-
-/** Fuller lines for the paper-and-ink styles, where a hairline reads as dust. */
-const OUTLINE_SCALE_LINE = { nose: 0.9, leaf: 1.9, vein: 2.4 };
-
-/**
- * Parts whose outline should only survive where the part itself sticks out past
- * the body. Their hull is drawn first with depth testing off, so everything the
- * body covers paints over it: the nose then only gets a line in the views where
- * it actually pokes out of the silhouette, instead of a ring around it - which is
- * what the original artwork does, the nose reads by its own colour there.
- *
- * In the line-art styles there is no colour to read it by (the nose is paper on
- * paper), so this is switched off and the nose goes to the screen-space pass
- * instead - see `screenParts` in main.js.
- */
-const OUTLINE_SILHOUETTE_ONLY = new Set(['nose']);
-
-export class Styles {
- constructor({ meshes, paper = '#ffffff', outlineColor = '#2a1e33', outlineWidth = 0.022 }) {
- this.meshes = meshes;
- this.paper = paper;
- this.style = 'real';
- this.outlineEnabled = true;
- /**
- * Source meshes whose hull outline must stay off, because another method
- * (the screen-space pass) is drawing them. Empty = every hull is available.
- * Only the leaves ever land here: see MODEL-GUIDE.md §5.
- */
- this.hullHidden = new Set();
-
- this.originals = new Map();
- for (const mesh of meshes) {
- this.originals.set(mesh, mesh.material);
- mesh.castShadow = true;
- mesh.receiveShadow = false;
- }
-
- this.gradientMap = makeGradientMap();
- this.toonMaterials = new Map();
- this.paperMaterial = new THREE.MeshBasicMaterial({ color: paper, toneMapped: false });
- // A hat is not flat: in a line drawing a white hat on a white head merges
- // into one shape, so hats get a light tone instead of paper (see addMesh).
- // The tone is per-mesh (`toneMeshes`, mesh -> material) and the amount it
- // steps from the paper towards the ink is cached by strength in
- // `toneMaterials`, so a part on a same-coloured neighbour (a hat band, a
- // pencil's lead) can ask for a darker fill and its seam then reads.
- this.toneMaterials = new Map();
- /** Runtime meshes that want a tone fill rather than paper in 線画. */
- this.toneMeshes = new Map();
- this.invisibleMaterial = new THREE.MeshBasicMaterial({ colorWrite: false, depthWrite: true });
-
- this.outlineWidth = outlineWidth;
- this.outlineColor = outlineColor;
- /** scale -> { material, uniform }, so the width slider updates all of them. */
- this.hullMaterials = new Map();
- this.outlineMeshes = this.createOutlines();
- this.setStyle('real');
- }
-
- /**
- * The part kind, taken from the material the GLB shipped with.
- *
- * It must be read from `originals`, not from `mesh.material`: `setStyle`
- * replaces the materials, and the replacements (paper, ink, toon) carry no
- * name, so after the first swap every part would look like an unknown one and
- * the leaf/nose special cases would silently stop applying.
- */
- kindOf(mesh) {
- return (this.originals.get(mesh)?.name ?? '').toLowerCase();
- }
-
- hullMaterialFor(scale, overlay = false) {
- const key = `${scale}:${overlay ? 'overlay' : 'solid'}`;
- let entry = this.hullMaterials.get(key);
- if (!entry) {
- const uniform = { value: this.outlineWidth * scale };
- const material = makeHullMaterial(this.outlineColor, uniform);
- if (overlay) {
- // Depth-*tested*, but writing none: the hull is drawn before the body
- // (renderOrder -1), so the body still paints over its interior and the
- // nose only gets a line where it pokes out of the silhouette. Testing
- // (rather than ignoring) depth is what lets the 見えない壁 hide it too -
- // with the test off, a nose buried in the wall left a filled blob,
- // because nothing was left to paint over the hull's inside.
- material.depthTest = true;
- material.depthWrite = false;
- }
- entry = { uniform, material, scale };
- this.hullMaterials.set(key, entry);
- }
- return entry.material;
- }
-
- /**
- * The flat fill a runtime mesh wears instead of paper in 線画, cached by how far
- * it steps from the paper towards the ink. `userData.tone` on the mesh (set by
- * the builder) carries the amount; the hat default comes from `options.tone`.
- */
- toneMaterialFor(strength = DEFAULT_TONE) {
- const key = String(strength);
- let material = this.toneMaterials.get(key);
- if (!material) {
- material = new THREE.MeshBasicMaterial({
- color: toneOf(this.paper, strength),
- toneMapped: false,
- });
- material.userData.tone = strength;
- this.toneMaterials.set(key, material);
- }
- return material;
- }
-
- createOutlines() {
- const hulls = [];
- for (const mesh of this.meshes) {
- const kind = this.kindOf(mesh);
- if (OUTLINE_SKIP.has(kind)) continue;
- const overlay = OUTLINE_SILHOUETTE_ONLY.has(kind);
- const material = this.hullMaterialFor(this.scaleFor(kind), overlay);
- const hull = mesh.isSkinnedMesh
- ? new THREE.SkinnedMesh(mesh.geometry, material)
- : new THREE.Mesh(mesh.geometry, material);
- hull.name = `${mesh.name || 'mesh'}:outline`;
- hull.position.copy(mesh.position);
- hull.quaternion.copy(mesh.quaternion);
- hull.scale.copy(mesh.scale);
- hull.frustumCulled = false;
- hull.castShadow = false;
- hull.receiveShadow = false;
- if (overlay) hull.renderOrder = -1;
- hull.userData.kind = kind;
- hull.userData.source = mesh;
- hull.userData.lineOnly = OUTLINE_LINE_ONLY.has(kind);
- hull.userData.shadedOnly = OUTLINE_SHADED_ONLY.has(kind);
- if (hull.isSkinnedMesh) hull.bind(mesh.skeleton, mesh.bindMatrix);
- mesh.parent.add(hull);
- hulls.push(hull);
- }
- return hulls;
- }
-
- /**
- * Register a mesh that is built at runtime (a hat), so it follows the render
- * styles and gets an outline hull like the GLB parts. `removeMesh` undoes it.
- *
- * `options.tone` gives the mesh the light tone fill in 線画 instead of paper,
- * for parts whose shape would otherwise merge with the body (the hats). A
- * number instead of `true` picks how far to step towards the ink, and the
- * builder can override it per mesh with `userData.tone` - which is how a part
- * draws its seam against a same-coloured neighbour.
- *
- * `options.lineOnly` keeps the hull to the line-art styles, so a runtime mesh
- * (a hat, a prop) gets its ink outline in 線画 but none in リアル / フラット.
- */
- addMesh(mesh, options = {}) {
- if (!mesh || this.originals.has(mesh)) return;
- this.originals.set(mesh, mesh.material);
- this.meshes.push(mesh);
- const tone = mesh.userData.tone ?? options.tone;
- if (tone) this.toneMeshes.set(mesh, this.toneMaterialFor(tone === true ? DEFAULT_TONE : tone));
- const kind = this.kindOf(mesh);
- if (!OUTLINE_SKIP.has(kind)) {
- const overlay = OUTLINE_SILHOUETTE_ONLY.has(kind);
- const hull = new THREE.Mesh(mesh.geometry, this.hullMaterialFor(this.scaleFor(kind), overlay));
- hull.name = `${mesh.name || 'mesh'}:outline`;
- hull.position.copy(mesh.position);
- hull.quaternion.copy(mesh.quaternion);
- hull.scale.copy(mesh.scale);
- hull.frustumCulled = false;
- hull.castShadow = false;
- hull.receiveShadow = false;
- if (overlay) hull.renderOrder = -1;
- hull.userData.kind = kind;
- hull.userData.source = mesh;
- hull.userData.lineOnly = options.lineOnly === true || OUTLINE_LINE_ONLY.has(kind);
- hull.userData.shadedOnly = OUTLINE_SHADED_ONLY.has(kind);
- mesh.parent?.add(hull);
- this.outlineMeshes.push(hull);
- mesh.userData.hull = hull;
- }
- this.setStyle(this.style);
- }
-
- /** Take a runtime mesh (and its outline hull) back out. */
- removeMesh(mesh) {
- if (!mesh || !this.originals.has(mesh)) return;
- const original = this.originals.get(mesh);
- const hull = mesh.userData.hull;
- if (hull) {
- hull.removeFromParent();
- const i = this.outlineMeshes.indexOf(hull);
- if (i >= 0) this.outlineMeshes.splice(i, 1);
- delete mesh.userData.hull;
- }
- // Put the mesh's own material back before dropping it: while a line style is
- // on it is wearing the *shared* paper/tone material, and the caller is about
- // to dispose it - which would blank every part using it.
- if (original) mesh.material = original;
- this.originals.delete(mesh);
- this.toneMeshes.delete(mesh);
- const j = this.meshes.indexOf(mesh);
- if (j >= 0) this.meshes.splice(j, 1);
- }
-
- /** The outline factor for a part in the current style (line art wants more). */
- scaleFor(kind) {
- const line = LINE_STYLES.has(this.style);
- const table = line ? OUTLINE_SCALE_LINE : OUTLINE_SCALE;
- return table[kind] ?? OUTLINE_SCALE[kind] ?? 1;
- }
-
- setStyle(value) {
- this.style = value;
- const line = LINE_STYLES.has(value);
- for (const mesh of this.meshes) {
- const original = this.originals.get(mesh);
- let material;
- if (line) {
- // Paper on paper in `lineart`; a light tone for hats, which would
- // otherwise merge into the head; invisible in `outline`, which is the pass
- // the SVG trace reads. Either way every edge - the body, the nose, and
- // the leaves - is drawn by the screen-space outline, so no part needs a
- // material trick of its own any more.
- material = value === 'lineart'
- ? (this.toneMeshes.get(mesh) ?? this.paperMaterial)
- : this.invisibleMaterial;
- } else if (value === 'flat') {
- material = this.toonFor(original);
- } else {
- material = original;
- }
- if (mesh.material !== material) mesh.material = material;
- }
-
- // The nose only keeps a line where it pokes out of the body in the shaded
- // styles; in a line drawing it is handed to the screen-space pass instead
- // (see `screenParts` in main.js), because a hull cannot ring a bump that sits
- // flush on the surface it is drawn on - the expanded shell lands *inside* the
- // body and loses the depth test.
- for (const hull of this.outlineMeshes) {
- const kind = hull.userData.kind;
- const silhouetteOnly = OUTLINE_SILHOUETTE_ONLY.has(kind) && !line;
- hull.material = this.hullMaterialFor(this.scaleFor(kind), silhouetteOnly);
- hull.renderOrder = silhouetteOnly ? -1 : 0;
- }
-
- // Lines are the whole point of the line-art styles.
- this.setOutlineVisible(line ? true : this.outlineEnabled);
- }
-
- /**
- * Hand a set of parts to the screen-space outline, or take them back.
- *
- * The hull and the screen-space pass each have a shape they cannot draw: a hull
- * cannot outline a thin closed shell (the leaves), and the screen-space pass
- * draws a stepped line because it works on the pixel grid. So the leaves go to
- * the screen-space pass and everything else keeps its hull, which is drawn from
- * the geometry and therefore comes out smooth.
- */
- setHullHidden(meshes) {
- this.hullHidden = new Set(meshes ?? []);
- this.setStyle(this.style);
- }
-
- toonFor(original) {
- let material = this.toonMaterials.get(original);
- if (!material) {
- material = new THREE.MeshToonMaterial({
- color: original.color ? original.color.clone() : new THREE.Color(0xffffff),
- map: original.map ?? null,
- vertexColors: original.vertexColors === true,
- gradientMap: this.gradientMap,
- side: original.side,
- transparent: original.transparent === true,
- alphaTest: original.alphaTest ?? 0,
- depthWrite: original.depthWrite !== false,
- });
- this.toonMaterials.set(original, material);
- }
- return material;
- }
-
- setPaper(color) {
- this.paper = color;
- this.paperMaterial.color.set(color);
- for (const material of this.toneMaterials.values()) {
- material.color.copy(toneOf(color, material.userData.tone));
- }
- }
-
- setOutlineVisible(visible) {
- const lineMode = LINE_STYLES.has(this.style);
- for (const hull of this.outlineMeshes) {
- const lineOnly = hull.userData.lineOnly === true;
- const shadedOnly = hull.userData.shadedOnly === true;
- hull.visible = visible
- && (!lineOnly || lineMode)
- && !(shadedOnly && lineMode)
- && !this.hullHidden.has(hull.userData.source);
- }
- }
-
- setOutlineEnabled(enabled) {
- this.outlineEnabled = enabled;
- if (!LINE_STYLES.has(this.style)) this.setOutlineVisible(enabled);
- }
-
- setOutlineWidth(width) {
- this.outlineWidth = width;
- for (const entry of this.hullMaterials.values()) entry.uniform.value = width * entry.scale;
- }
-
- setOutlineColor(color) {
- this.outlineColor = color;
- for (const entry of this.hullMaterials.values()) entry.material.color.set(color);
- }
-
- /**
- * The toon materials are cached copies of the originals, so a colour theme that
- * edits `original.color` has to be copied across or the flat style keeps
- * showing the old colour. See src/look.js.
- */
- refreshColors() {
- for (const [mesh, original] of this.originals) {
- const toon = this.toonMaterials.get(original);
- if (toon && original.color) toon.color.copy(original.color);
- // The outline hull is a colour of its own, so it is left alone.
- void mesh;
- }
- return this;
- }
-
- dispose() {
- for (const hull of this.outlineMeshes) {
- hull.removeFromParent();
- hull.skeleton = null;
- }
- this.outlineMeshes = [];
- for (const entry of this.hullMaterials.values()) entry.material.dispose();
- this.hullMaterials.clear();
- this.paperMaterial.dispose();
- for (const material of this.toneMaterials.values()) material.dispose();
- this.toneMaterials.clear();
- this.invisibleMaterial.dispose();
- this.gradientMap.dispose();
- for (const material of this.toonMaterials.values()) material.dispose();
- this.toonMaterials.clear();
- }
-}
-
-/** How far the hat default steps from the paper towards the ink in 線画. */
-const DEFAULT_TONE = 0.14;
-
-/** A step towards the ink, for the flat fill a runtime mesh gets in 線画. */
-function toneOf(paper, strength = DEFAULT_TONE) {
- return new THREE.Color(paper).lerp(new THREE.Color('#2a1e33'), strength);
-}
-
-/** A 3-step ramp gives crisper cartoon bands than the shader's default. */
-function makeGradientMap() {
- const steps = new Uint8Array([90, 165, 255]);
- const texture = new THREE.DataTexture(steps, steps.length, 1, THREE.RedFormat);
- texture.minFilter = THREE.NearestFilter;
- texture.magFilter = THREE.NearestFilter;
- texture.generateMipmaps = false;
- texture.needsUpdate = true;
- return texture;
-}
-
-function makeHullMaterial(color, uniform) {
- const material = new THREE.MeshBasicMaterial({
- color,
- side: THREE.BackSide,
- toneMapped: false,
- });
- material.onBeforeCompile = (shader) => {
- shader.uniforms.uOutline = uniform;
- shader.vertexShader = `uniform float uOutline;\n${shader.vertexShader}`.replace(
- '#include <begin_vertex>',
- '#include <begin_vertex>\n\ttransformed += normal * uOutline;',
- );
- };
- material.customProgramCacheKey = () => 'bluebey-outline-hull';
- return material;
-}