From bc2821adb11a30244fc4663f4fafb756877d9508 Mon Sep 17 00:00:00 2001 From: Yasutake Yohei <61961825+yasutakeyohei@users.noreply.github.com> Date: Wed, 7 Oct 2026 23:09:40 +0900 Subject: bluebey-studio: public/ の外へ移動し非公開化 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- public/bluebey-studio/src/styles.js | 457 ------------------------------------ 1 file changed, 457 deletions(-) delete mode 100644 public/bluebey-studio/src/styles.js (limited to 'public/bluebey-studio/src/styles.js') 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 ', - '#include \n\ttransformed += normal * uOutline;', - ); - }; - material.customProgramCacheKey = () => 'bluebey-outline-hull'; - return material; -} -- cgit v1.3.1