diff options
Diffstat (limited to 'public/bluebey-studio/src/look.js')
| -rw-r--r-- | public/bluebey-studio/src/look.js | 184 |
1 files changed, 0 insertions, 184 deletions
diff --git a/public/bluebey-studio/src/look.js b/public/bluebey-studio/src/look.js deleted file mode 100644 index d755c3b..0000000 --- a/public/bluebey-studio/src/look.js +++ /dev/null @@ -1,184 +0,0 @@ -import * as THREE from 'three'; -import { RoomEnvironment } from 'three/addons/environments/RoomEnvironment.js'; - -/** - * The "look" of the scene, as opposed to the character's shape: body colours, - * mirroring, shadows and the lighting environment. - * - * These all live here rather than in `main.js` because they are a *policy* about - * how the model should be presented, and they need to be re-applied as a group - * whenever any of them changes. `main.js` only has to call `apply()`. - * - * Two things are worth knowing: - * - * - Colours are written onto the model's ORIGINAL materials. The flat/toon - * style caches its own materials derived from them, so `styles.refreshColors()` - * has to be called afterwards or the toon shading keeps the old colour. - * - * - Shadow softness is `LightShadow.radius` with a PCF soft shadow map, plus a - * hand-drawn contact blob for the "ground shadow only" mode. (VSM was tried - * for its wider blur and cut the shadow off in a straight line where it met - * the feet - see the note in `apply`.) - */ - -export const ENVIRONMENTS = [ - { value: 'gradient', label: 'スタジオ(明るい)' }, - { value: 'room', label: '室内(自然な反射)' }, - { value: 'none', label: 'なし(のっぺり)' }, -]; - -/** Material name -> which entry of `render.colors` it takes. */ -const PART_COLORS = { - blb: 'body', - Hand: 'accent', - Foots: 'feet', - Nose: 'nose', - Leaf: 'leaf', - Vein: 'vein', -}; - -const clamp01 = (value) => Math.min(1, Math.max(0, Number(value) || 0)); - -/** - * A soft blob that sits under the feet: the shadow a toy would cast on a table. - * It is a plain plane with a radial gradient, so it is cheap and it works in the - * line-art styles too, where the real shadow map is switched off. - */ -function makeContactShadow(size) { - const canvas = document.createElement('canvas'); - canvas.width = 128; - canvas.height = 128; - const ctx = canvas.getContext('2d'); - const gradient = ctx.createRadialGradient(64, 64, 4, 64, 64, 62); - gradient.addColorStop(0, 'rgba(0,0,0,0.55)'); - gradient.addColorStop(0.55, 'rgba(0,0,0,0.28)'); - gradient.addColorStop(1, 'rgba(0,0,0,0)'); - ctx.fillStyle = gradient; - ctx.fillRect(0, 0, 128, 128); - - const texture = new THREE.CanvasTexture(canvas); - texture.colorSpace = THREE.SRGBColorSpace; - const material = new THREE.MeshBasicMaterial({ - map: texture, - transparent: true, - depthWrite: false, - toneMapped: false, - }); - const mesh = new THREE.Mesh(new THREE.PlaneGeometry(1, 1), material); - mesh.rotation.x = -Math.PI / 2; - mesh.renderOrder = -0.5; - mesh.userData.isHelper = true; - mesh.name = 'contact-shadow'; - mesh.scale.setScalar(Math.max(1, size.x) * 1.15); - return mesh; -} - -export function createLook({ renderer, scene, styles, model, ground, key, character, gradientEnvironment }) { - const pmrem = new THREE.PMREMGenerator(renderer); - const roomEnvironment = pmrem.fromScene(new RoomEnvironment(), 0.06).texture; - - const contact = makeContactShadow(model.size); - scene.add(contact); - - const environmentFor = (name) => { - if (name === 'room') return roomEnvironment; - if (name === 'none') return null; - return gradientEnvironment ?? null; - }; - - function applyColors(colors) { - if (!colors) return; - let touched = false; - for (const mesh of model.parts.body) { - const material = styles.originals.get(mesh) ?? mesh.material; - const part = PART_COLORS[material?.name]; - if (!part || !colors[part]) continue; - if (!material.color) material.color = new THREE.Color(); - material.color.set(colors[part]); - touched = true; - } - // The toon materials are cached copies, so they need the same edit. - if (touched) styles.refreshColors?.(); - return touched; - } - - return { - contact, - - /** - * @param {object} render `state.render` - * @param {{ line?: boolean }} [context] `line` is true in the line-art - * styles, where shadows and reflections are deliberately switched off. - */ - apply(render, { line = false } = {}) { - if (!render) return; - applyColors(render.colors); - - // Mirroring by negative scale is safe: the renderer flips the winding for - // a negative determinant, and the normals go through the inverse-transpose. - character.scale.x = render.mirror ? -1 : 1; - - const wantsShadow = render.shadow !== false && !line; - const blobOnly = render.contactShadow === true; - ground.visible = wantsShadow && !blobOnly; - ground.material.opacity = clamp01(render.shadowOpacity ?? 0.22); - key.castShadow = wantsShadow && !blobOnly; - key.shadow.needsUpdate = true; - - // The blob is drawn in BOTH modes. On its own it *is* the shadow (the - // "ground shadow only" setting); with the shadow map on it fills the gap - // where the body hides its own shadow right at the feet, which otherwise - // reads as "the shadow is cut off". It is the only thing keeping the - // character looking like it is standing on the floor rather than above it. - // The blob sits at the feet. It used to be drawn whenever the shadow was on, - // which left a faint ring behind at the origin once the character was moved; - // it is now opt-in (`render.contactBlob`, default off). It is always shown in - // the 「接地影だけ」 mode, where it is the only shadow there is. - contact.visible = wantsShadow && (blobOnly || render.contactBlob === true); - contact.material.opacity = clamp01((render.shadowOpacity ?? 0.22) * (blobOnly ? 2.6 : 1.6)); - const spread = Math.max(0.2, (model.size.x * 1.15) / Math.max(0.05, render.shadowSoftness ?? 1.6)); - contact.scale.setScalar(Math.max(0.5, model.size.x * 1.35 - spread * 0.25)); - contact.position.y = 0.012; - - const softness = Number(render.shadowSoftness) || 1.6; - // PCF, always. three 0.186 removed PCFSoftShadowMap (asking for it warns and - // falls back to this anyway), and VSM needs a depth-variance bias that cut - // the shadow away in a straight line right at the feet. `radius` is what - // softens PCF. - if (renderer.shadowMap.type !== THREE.PCFShadowMap) { - renderer.shadowMap.type = THREE.PCFShadowMap; - // Every shadow-receiving material has to be recompiled for the switch. - ground.material.needsUpdate = true; - for (const mesh of model.parts.body) { - const material = mesh.material; - if (Array.isArray(material)) material.forEach((m) => { m.needsUpdate = true; }); - else if (material) material.needsUpdate = true; - } - } - // PCF's radius is in *shadow map texels*, and the map covers about 15 world - // units, so a radius of 1-2 is invisible. This scales it into something the - // eye can see without the sampling turning into noise. - if ('radius' in key.shadow) key.shadow.radius = Math.max(0.5, softness * 5); - - const environment = line ? null : environmentFor(render.environment); - if (scene.environment !== environment) scene.environment = environment; - if ('environmentIntensity' in scene) { - scene.environmentIntensity = Number(render.envIntensity ?? 1); - } - }, - - /** The colours a theme would set, as a plain `{ part: '#rrggbb' }`. */ - palette(theme) { - return { ...(theme?.colors ?? {}) }; - }, - - dispose() { - contact.geometry.dispose(); - contact.material.map?.dispose(); - contact.material.dispose(); - contact.removeFromParent(); - roomEnvironment.dispose(); - pmrem.dispose(); - }, - }; -} |
