@7ots/cli 0.1.0 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/.env.example +17 -1
  2. package/cli/7ots.mjs +136 -36
  3. package/cli/lib/assistant.mjs +337 -0
  4. package/cli/lib/brain.mjs +10 -7
  5. package/cli/lib/i18n.mjs +87 -17
  6. package/cli/lib/install.mjs +29 -1
  7. package/cli/lib/pet-core.mjs +3 -2
  8. package/cli/lib/pet-server.mjs +30 -0
  9. package/cli/lib/prompts.mjs +200 -8
  10. package/cli/lib/terminal.mjs +27 -6
  11. package/cli/pet/electron/main.cjs +79 -28
  12. package/cli/pet/pet.html +55 -6
  13. package/dist/7ots.esm.js +370 -142
  14. package/dist/7ots.esm.js.map +4 -4
  15. package/dist/7ots.iife.js +370 -142
  16. package/dist/7ots.iife.js.map +4 -4
  17. package/llms.txt +199 -7
  18. package/package.json +1 -2
  19. package/server/channels/apuchat.mjs +8 -5
  20. package/server/channels/apumail.mjs +13 -3
  21. package/server/channels/index.mjs +5 -3
  22. package/server/platform/apuchat.mjs +266 -0
  23. package/server/platform/auth.mjs +39 -10
  24. package/server/platform/connect.mjs +347 -0
  25. package/server/platform/db.mjs +55 -0
  26. package/server/platform/look.mjs +20 -0
  27. package/server/platform/orquesta.mjs +499 -0
  28. package/server/platform/routes.mjs +57 -8
  29. package/server/platform/runtime.mjs +75 -15
  30. package/server/platform/store.mjs +130 -9
  31. package/server/server.mjs +14 -3
  32. package/skills/7ots/SKILL.md +1 -1
  33. package/src/avatar/AvatarStage.js +54 -9
  34. package/src/avatar/three.js +62 -0
  35. package/src/character/AvatarEditor.js +174 -50
  36. package/src/character/Character.js +164 -26
  37. package/src/character/motion.js +95 -34
  38. package/src/character/parts.js +43 -12
  39. package/src/character/skin.js +412 -0
  40. package/src/character/styles.js +1429 -0
  41. package/src/character3d/Character3D.js +1693 -0
  42. package/src/character3d/body.js +154 -0
  43. package/src/character3d/fx.js +323 -0
  44. package/src/character3d/index.js +281 -0
  45. package/src/character3d/materials.js +429 -0
  46. package/src/character3d/stage.js +187 -0
  47. package/src/i18n/messages/character.js +18 -42
  48. package/src/i18n/messages/platform.js +135 -0
  49. package/src/identity/schema.js +16 -8
  50. package/src/index.js +1 -0
@@ -0,0 +1,429 @@
1
+ /**
2
+ * Materiales de los ots 3D.
3
+ *
4
+ * Peluche (plush): pelo por «shell texturing». El cuerpo se dibuja una vez sólido (raíces en
5
+ * sombra) y N veces más como capas desplazadas a lo largo de la normal (un único InstancedMesh:
6
+ * gl_InstanceID → altura de la capa). En cada capa un ruido celular 3D en espacio objeto decide
7
+ * dónde hay hebra; las hebras se afinan y se acortan al azar, caen un poco con la gravedad y se
8
+ * mecen. alphaToCoverage + MSAA suaviza los bordes sin ordenar transparencias. El número de
9
+ * capas se ajusta por personaje según su tamaño en pantalla (LOD).
10
+ *
11
+ * Sobre MeshPhysicalMaterial (PBR, sheen para el brillo aterciopelado del peluche) vía
12
+ * onBeforeCompile: el estampado (barriga, dos tonos, manchas, rayas, antifaz), la textura de la
13
+ * piel (escamas, motas, anillos, gelatina, paneles: color + relieve + rugosidad) y las mejillas
14
+ * (rubor, rayitas, pecas, pintura, corazones, estrellas) se pintan en el shader, así también
15
+ * «tiñen» el pelo. Las metamorfosis de deformación (derretirse, gelatina, shape:<id>) también
16
+ * van en el vertex shader, compartidas por cuerpo, capas, contorno y parches.
17
+ *
18
+ * Vinilo (glossy/soft3d/clay/flat/toon/neon/sketch): mismo shader sin pelo, con clearcoat/
19
+ * rugosidad según el acabado; toon/sketch cuantizan la luz (y sketch añade sombreado a rayas).
20
+ */
21
+
22
+ import * as THREE from 'three';
23
+
24
+ export const PATTERN_IDS = { none: 0, belly: 1, twotone: 2, spots: 3, stripes: 4, tiger: 5, mask: 6, hearts: 3, stars: 3, circuit: 4 };
25
+ export const TEX_IDS = { none: 0, scales: 1, speckles: 2, ridges: 3, gloss: 4, panels: 5 };
26
+ export const CHEEK_IDS = { none: 0, blush: 1, lines: 2, freckles: 3, paint: 4, hearts: 5, stars: 6 };
27
+
28
+ const COMMON = /* glsl */ `
29
+ uniform vec3 uPat; uniform int uPatType; uniform vec4 uDims; // W, H, eyeY, eyeR
30
+ uniform vec4 uCheek; uniform vec3 uCheekColor; uniform float uCheekOn; uniform int uCheekType;
31
+ uniform float uDensity; uniform float uShells; uniform float uFurLen; uniform float uTime;
32
+ uniform vec4 uBald[3];
33
+ uniform int uTex; uniform float uSp; uniform float uBlush;
34
+ uniform vec4 uMorph; // derretir, gelatina, shape, -
35
+ varying vec3 vFurP; varying float vH; varying vec3 vON;
36
+ float furJ = 0.5;
37
+ float texH = 0.0; float texR = 0.0;
38
+ vec3 h33(vec3 p) {
39
+ p = vec3(dot(p, vec3(127.1, 311.7, 74.7)), dot(p, vec3(269.5, 183.3, 246.1)), dot(p, vec3(113.5, 271.9, 124.6)));
40
+ return fract(sin(p) * 43758.5453123);
41
+ }
42
+ float patMask(vec3 P) {
43
+ float W = uDims.x, H = uDims.y;
44
+ if (uPatType == 1) { // barriga
45
+ vec2 q = vec2(P.x / (W * 0.33), (P.y - H * 0.33) / (H * 0.29));
46
+ return (1.0 - smoothstep(0.9, 1.05, length(q))) * smoothstep(-0.05, 0.12, P.z);
47
+ }
48
+ if (uPatType == 2) return 1.0 - smoothstep(H * 0.4, H * 0.43, P.y);
49
+ if (uPatType == 3) {
50
+ vec3 q = P * (4.2 / W); vec3 c = floor(q); vec3 r = h33(c);
51
+ return r.x > 0.45 ? 1.0 - smoothstep(0.2, 0.25, length(fract(q) - (0.3 + 0.4 * r))) : 0.0;
52
+ }
53
+ if (uPatType == 4) return step(0.55, sin(P.y / H * 38.0));
54
+ if (uPatType == 5) return step(0.45, sin(P.y / H * 30.0 + sin(P.x * 9.0) * 1.6)) * smoothstep(0.15 * W, 0.4 * W, abs(P.x) + 0.25 * W * step(P.z, 0.0));
55
+ if (uPatType == 6) return (1.0 - smoothstep(uDims.w * 1.6, uDims.w * 1.8, abs(P.y - uDims.z))) * smoothstep(-0.05, 0.1, P.z);
56
+ return 0.0;
57
+ }
58
+ float furA(vec3 p, float h, out float sh) {
59
+ float a = -1.0; sh = 1.0;
60
+ for (int i = 0; i < 3; i++) {
61
+ vec3 q = p + vec3(float(i) * 0.37, float(i) * 0.61, float(i) * 0.13);
62
+ vec3 c = floor(q);
63
+ vec3 r = h33(c + float(i) * 17.0);
64
+ float len = 0.5 + 0.5 * r.z;
65
+ float rad = 0.52 * (1.0 - 0.85 * h / len);
66
+ float d = length(fract(q) - (0.3 + 0.4 * r));
67
+ float v = h > len ? -1.0 : (rad - d) / max(rad, 1e-3);
68
+ if (v > a) { a = v; sh = 0.86 + 0.28 * r.x; furJ = r.y; }
69
+ }
70
+ return a;
71
+ }
72
+ // textura de la piel: devuelve un factor de color; deja relieve (texH) y rugosidad (texR)
73
+ float skinTex(vec3 P, vec3 N) {
74
+ float W = uDims.x, H = uDims.y;
75
+ if (uTex == 1) { // escamas solapadas (mapeo cilíndrico)
76
+ float s = W * 0.085;
77
+ vec2 q = vec2(atan(P.x, P.z) * W * 0.42, P.y) / s;
78
+ float best = 0.0;
79
+ for (int k = 0; k < 2; k++) {
80
+ float row = floor(q.y) + float(k);
81
+ vec2 f = vec2(q.x + 0.5 * mod(row, 2.0), q.y - row);
82
+ vec2 d = vec2(fract(f.x) - 0.5, f.y + 0.15);
83
+ float v = 1.0 - length(d * vec2(1.0, 1.25));
84
+ best = max(best, v);
85
+ }
86
+ texH = smoothstep(0.0, 0.45, best) * 0.9; texR = -0.18;
87
+ return mix(0.72, 1.08, smoothstep(0.0, 0.5, best));
88
+ }
89
+ if (uTex == 2) { // motas
90
+ vec3 q = P * (13.0 / W); vec3 c = floor(q); vec3 r = h33(c + 3.1);
91
+ float sp = r.x > 0.35 ? 1.0 - smoothstep(0.1, 0.16, length(fract(q) - (0.25 + 0.5 * r)) / (0.6 + r.z)) : 0.0;
92
+ texH = -sp * 0.4;
93
+ return mix(1.0, r.y > 0.5 ? 0.62 : 1.22, sp);
94
+ }
95
+ if (uTex == 3) { // anillos
96
+ float b = 0.5 + 0.5 * sin(P.y / H * 6.2832 * 9.0);
97
+ texH = smoothstep(0.15, 0.85, b) * 0.8; texR = 0.05;
98
+ return mix(0.8, 1.04, texH);
99
+ }
100
+ if (uTex == 4) { texR = -0.45; return 1.0; } // gelatina: el resto en el material y la luz
101
+ if (uTex == 5) { // paneles atornillados (triplanar)
102
+ float s = W * 0.3;
103
+ vec3 a = abs(N);
104
+ vec2 q = a.z > a.x && a.z > a.y ? P.xy : a.x > a.y ? P.zy : P.xz;
105
+ vec2 f = fract(q / s + 0.5) - 0.5;
106
+ float e = min(0.5 - abs(f.x), 0.5 - abs(f.y)) * s;
107
+ float groove = 1.0 - smoothstep(0.0, W * 0.012, e);
108
+ float rivet = 1.0 - smoothstep(W * 0.01, W * 0.016, length(abs(f) * s - vec2(0.5 * s - W * 0.035)));
109
+ texH = -groove * 0.7 + rivet * 0.8; texR = -0.15 + groove * 0.3;
110
+ return mix(1.0, 0.7, groove) * mix(1.0, 1.15, rivet);
111
+ }
112
+ return 1.0;
113
+ }
114
+ float sdSeg(vec2 p, vec2 a, vec2 b) { vec2 pa = p - a, ba = b - a; return length(pa - ba * clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0)); }
115
+ float sdHeart(vec2 p) { // iq; p en unidades del corazón (arriba = +y)
116
+ p.x = abs(p.x); p.y += 0.6;
117
+ if (p.y + p.x > 1.0) return sqrt(dot(p - vec2(0.25, 0.75), p - vec2(0.25, 0.75))) - 0.3536;
118
+ return sqrt(min(dot(p - vec2(0.0, 1.0), p - vec2(0.0, 1.0)), dot(p - 0.5 * max(p.x + p.y, 0.0), p - 0.5 * max(p.x + p.y, 0.0)))) * sign(p.x - p.y);
119
+ }
120
+ float sdStar(vec2 p, float r) { // estrella de 5 puntas (iq)
121
+ const vec2 k1 = vec2(0.809016994375, -0.587785252292);
122
+ const vec2 k2 = vec2(-0.809016994375, -0.587785252292);
123
+ p.x = abs(p.x);
124
+ p -= 2.0 * max(dot(k1, p), 0.0) * k1;
125
+ p -= 2.0 * max(dot(k2, p), 0.0) * k2;
126
+ p.x = abs(p.x);
127
+ p.y -= r;
128
+ vec2 ba = 0.45 * vec2(-k1.y, k1.x) - vec2(0.0, 1.0);
129
+ float h = clamp(dot(p, ba) / dot(ba, ba), 0.0, r);
130
+ return length(p - ba * h) * sign(p.y * ba.x - p.x * ba.y);
131
+ }
132
+ // mejillas y rubor: devuelve color y peso
133
+ vec4 cheekPaint(vec3 P) {
134
+ float r = uDims.w;
135
+ vec2 q = (vec2(abs(P.x), P.y) - uCheek.xy) / r; // unidades de radio del ojo, arriba = +y
136
+ float front = smoothstep(-0.02, 0.06, P.z);
137
+ vec4 o = vec4(0.0);
138
+ if (uCheekOn > 0.5) {
139
+ float a = 0.0;
140
+ if (uCheekType == 1) { float cd = length(vec3(abs(P.x), P.y, P.z) - uCheek.xyz) / uCheek.w; a = 0.7 * exp(-cd * cd * 2.2); }
141
+ else if (uCheekType == 2) { float d = 9.0; for (int i = 0; i < 3; i++) { vec2 b = vec2(float(i - 1) * 0.4 - 0.12, -0.25); d = min(d, sdSeg(q, b, b + vec2(0.25, 0.5))); } a = 1.0 - smoothstep(0.06, 0.1, d); }
142
+ else if (uCheekType == 3) {
143
+ float d = 9.0;
144
+ vec2 F[5]; F[0] = vec2(0.0); F[1] = vec2(0.5, -0.3); F[2] = vec2(-0.4, -0.4); F[3] = vec2(0.2, -0.75); F[4] = vec2(-0.2, 0.1);
145
+ for (int i = 0; i < 5; i++) d = min(d, length(q - F[i] - vec2(-0.25, 0.1)));
146
+ a = 1.0 - smoothstep(0.08, 0.12, d);
147
+ }
148
+ else if (uCheekType == 4) { float d = min(sdSeg(q, vec2(-0.65, 0.4), vec2(0.95, 0.4)), sdSeg(q, vec2(-0.65, -0.1), vec2(0.95, -0.1))); a = (1.0 - smoothstep(0.1, 0.14, d)) * 0.95; }
149
+ else if (uCheekType == 5) a = 1.0 - smoothstep(-0.02, 0.03, sdHeart((q - vec2(0.05, -0.1)) / 0.5) * 0.5);
150
+ else if (uCheekType == 6) a = 1.0 - smoothstep(-0.01, 0.03, sdStar(q - vec2(0.15, -0.1), 0.42));
151
+ o = vec4(uCheekColor, a * front);
152
+ }
153
+ if (uBlush > 0.001) {
154
+ vec2 b = (vec2(abs(P.x), P.y) - vec2(uSp + 0.35 * r, uDims.z - 1.8 * r)) / r;
155
+ float e = 1.0 - smoothstep(0.7, 1.0, length(b / vec2(0.95, 0.5)));
156
+ float d = 9.0;
157
+ for (int i = 0; i < 3; i++) { vec2 s = vec2(float(i - 1) * 0.4, -0.25); d = min(d, sdSeg(b, s, s + vec2(0.22, 0.45))); }
158
+ float l = 1.0 - smoothstep(0.05, 0.09, d);
159
+ vec3 c = mix(vec3(1.0, 0.365, 0.56), vec3(1.0, 0.23, 0.43), l);
160
+ float a = max(e * 0.55, l) * uBlush * front;
161
+ o = vec4(mix(o.rgb, c, a), max(o.a, a));
162
+ }
163
+ return o;
164
+ }
165
+ `;
166
+
167
+ /** Deformaciones (derretir, gelatina, shape:<id>) — mismas en cuerpo, capas, contorno y parches. */
168
+ const DEFORM = /* glsl */ `
169
+ attribute vec3 aShape; attribute vec3 aShapeN;
170
+ vec3 otDeform(vec3 p, inout vec3 n) {
171
+ if (uMorph.z > 0.0) { p = mix(p, aShape, uMorph.z); n = normalize(mix(n, aShapeN, uMorph.z)); }
172
+ float H = uDims.y, W = uDims.x;
173
+ if (uMorph.x > 0.0) { // derretir: la base se ensancha y gotea
174
+ float ny = clamp(1.0 - p.y / max(H, 1e-3), 0.0, 1.0);
175
+ float k = uMorph.x * smoothstep(0.45, 1.0, ny);
176
+ p.xz *= 1.0 + 0.55 * k;
177
+ p.y -= k * W * 0.05 * max(0.0, sin(atan(p.x, p.z) * 5.0 + uTime * 0.7));
178
+ }
179
+ if (uMorph.y > 0.0) p += n * W * 0.05 * uMorph.y * sin(uTime * 6.2832 * 2.6 + atan(p.y - H * 0.5, p.x) * 3.3 + p.z * 4.0);
180
+ return p;
181
+ }
182
+ `;
183
+
184
+ const VERT_HEAD = `#include <common>\n${COMMON}\n${DEFORM}\nuniform vec3 uGravity;`;
185
+ const VERT_BEGIN = /* glsl */ `vec3 dN = normal;
186
+ vec3 transformed = otDeform(vec3(position), dN);
187
+ vFurP = position; vH = 0.0; vON = normal;`;
188
+
189
+ /** Relieve por derivadas (sin mapas): perturba `normal` con el gradiente en pantalla de h. */
190
+ const BUMP = /* glsl */ `
191
+ if (uTex != 0) {
192
+ vec3 sp = -vViewPosition;
193
+ vec3 sx = dFdx(sp), sy = dFdy(sp);
194
+ float hh = texH * uDims.x * 0.012;
195
+ vec2 dh = vec2(dFdx(hh), dFdy(hh));
196
+ vec3 r1 = cross(sy, normal), r2 = cross(normal, sx);
197
+ float det = dot(sx, r1);
198
+ vec3 grad = sign(det) * (dh.x * r1 + dh.y * r2);
199
+ normal = normalize(abs(det) * normal - grad);
200
+ }`;
201
+
202
+ /**
203
+ * Crea el par de materiales del cuerpo (sólido + capas de pelo) con uniforms compartidos.
204
+ * @param {object} o { color, patternColor, pattern, finish, plush, furLen, density, cheekColor, texture }
205
+ */
206
+ export function bodyMaterials(o) {
207
+ const tex = TEX_IDS[o.texture] ?? 0;
208
+ const uniforms = {
209
+ uPat: { value: new THREE.Color(o.patternColor) },
210
+ uPatType: { value: PATTERN_IDS[o.pattern] ?? 0 },
211
+ uDims: { value: new THREE.Vector4(1, 1, 0.6, 0.1) },
212
+ uCheek: { value: new THREE.Vector4(0.3, 0.4, 0.4, 0.1) },
213
+ uCheekColor: { value: new THREE.Color(o.cheekColor || '#ff6b8a') },
214
+ uCheekOn: { value: 0 },
215
+ uCheekType: { value: 1 },
216
+ uDensity: { value: o.density || 70 },
217
+ uShells: { value: 16 },
218
+ uFurLen: { value: o.furLen || 0.06 },
219
+ uTime: { value: 0 },
220
+ uGravity: { value: new THREE.Vector3(0, -0.9, 0.15) },
221
+ uBald: { value: [new THREE.Vector4(0, -9, 0, 0), new THREE.Vector4(0, -9, 0, 0), new THREE.Vector4(0, -9, 0, 0)] },
222
+ uTex: { value: tex },
223
+ uSp: { value: 0.2 },
224
+ uBlush: { value: 0 },
225
+ uMorph: { value: new THREE.Vector4(0, 0, 0, 0) },
226
+ };
227
+ const toon = (o.finish === 'toon' || o.finish === 'sketch') && !o.plush;
228
+ const make = (shell) => {
229
+ const f = o.plush ? 'plush' : o.finish;
230
+ const col = new THREE.Color(o.color);
231
+ if (o.plush) col.multiplyScalar(1.12); // compensa la sombra entre hebras
232
+ const gel = tex === 4 && !o.plush;
233
+ const m = new THREE.MeshPhysicalMaterial({
234
+ color: col,
235
+ roughness: gel ? 0.08 : { plush: 0.92, glossy: 0.28, soft3d: 0.55, clay: 0.85, flat: 0.75, toon: 0.6, neon: 0.4, sketch: 0.9 }[f] ?? 0.5,
236
+ metalness: 0,
237
+ clearcoat: gel ? 1 : { glossy: 1, soft3d: 0.35, toon: 0.6, neon: 0.8 }[f] ?? 0,
238
+ clearcoatRoughness: f === 'soft3d' ? 0.35 : 0.08,
239
+ sheen: o.plush ? 1 : f === 'clay' ? 0.4 : 0,
240
+ sheenRoughness: 0.5,
241
+ sheenColor: new THREE.Color(o.color).lerp(new THREE.Color('#ffffff'), 0.45),
242
+ emissive: f === 'neon' ? new THREE.Color(o.color).multiplyScalar(0.55) : new THREE.Color(0),
243
+ envMapIntensity: o.plush ? 0.7 : gel ? 1.5 : 1,
244
+ });
245
+ if (shell) {
246
+ m.alphaTest = 0.5;
247
+ m.alphaToCoverage = true;
248
+ }
249
+ m.defines = {
250
+ ...(o.plush ? { FUR: '' } : {}),
251
+ ...(shell ? { FUR_SHELL: '' } : {}),
252
+ ...(toon && !shell ? { TOON: '' } : {}),
253
+ ...(o.finish === 'sketch' && !shell ? { SKETCH: '' } : {}),
254
+ ...(gel ? { GEL: '' } : {}),
255
+ };
256
+ m.customProgramCacheKey = () => `ots3d-${o.plush ? 'fur' : 'solid'}-${shell ? 'shell' : 'base'}-${toon ? o.finish : ''}-${gel ? 'gel' : ''}`;
257
+ m.onBeforeCompile = (sh) => {
258
+ Object.assign(sh.uniforms, uniforms);
259
+ sh.vertexShader = sh.vertexShader
260
+ .replace('#include <common>', VERT_HEAD)
261
+ .replace(
262
+ '#include <begin_vertex>',
263
+ /* glsl */ `${VERT_BEGIN}
264
+ #ifdef FUR_SHELL
265
+ float fh = (float(gl_InstanceID) + 1.0) / uShells;
266
+ vH = fh;
267
+ float wob = sin(uTime * 1.6 + position.y * 4.0 + position.x * 3.0) * 0.18;
268
+ vec3 fg = (uGravity + vec3(wob, 0.0, wob * 0.5)) * fh;
269
+ transformed += normalize(dN + fg) * (uFurLen * fh);
270
+ #endif`,
271
+ )
272
+ .replace('#include <beginnormal_vertex>', '#include <beginnormal_vertex>\nif (uMorph.z > 0.0) objectNormal = normalize(mix(objectNormal, aShapeN, uMorph.z));');
273
+ sh.fragmentShader = sh.fragmentShader
274
+ .replace('#include <common>', `#include <common>\n${COMMON}`)
275
+ .replace(
276
+ '#include <color_fragment>',
277
+ /* glsl */ `#include <color_fragment>
278
+ vec3 oP = vFurP;
279
+ diffuseColor.rgb = mix(diffuseColor.rgb, uPat, patMask(oP));
280
+ diffuseColor.rgb *= skinTex(oP, normalize(vON));
281
+ vec4 chk = cheekPaint(oP);
282
+ diffuseColor.rgb = mix(diffuseColor.rgb, chk.rgb, chk.a);
283
+ float oAO = uDims.y < 0.01 ? 1.0 : mix(0.5, 1.0, smoothstep(0.0, 0.3 * uDims.y, oP.y));
284
+ #ifdef FUR
285
+ float bald = 1.0;
286
+ for (int i = 0; i < 3; i++) bald = min(bald, smoothstep(uBald[i].w * 0.75, uBald[i].w * 1.35, length(oP - uBald[i].xyz)));
287
+ #ifdef FUR_SHELL
288
+ float fsh;
289
+ float fa = furA(oP * uDensity, vH / max(bald, 0.04), fsh);
290
+ diffuseColor.a = clamp(0.5 + fa * 2.0, 0.0, 1.0);
291
+ diffuseColor.rgb *= mix(0.42, 1.06, vH) * fsh;
292
+ #else
293
+ diffuseColor.rgb *= mix(0.9, 0.42, bald);
294
+ #endif
295
+ #endif
296
+ diffuseColor.rgb *= oAO;`,
297
+ )
298
+ .replace('#include <roughnessmap_fragment>', '#include <roughnessmap_fragment>\nroughnessFactor = clamp(roughnessFactor + texR, 0.04, 1.0);')
299
+ .replace(
300
+ '#include <normal_fragment_maps>',
301
+ /* glsl */ `#include <normal_fragment_maps>
302
+ #ifdef FUR_SHELL
303
+ normal = normalize(normal + (vec3(furJ, fract(furJ * 7.13), fract(furJ * 3.7)) - 0.5) * 0.8);
304
+ #else
305
+ ${BUMP}
306
+ #endif`,
307
+ )
308
+ .replace(
309
+ '#include <emissivemap_fragment>',
310
+ /* glsl */ `#include <emissivemap_fragment>
311
+ #ifdef GEL
312
+ float fres = pow(1.0 - abs(dot(normal, normalize(vViewPosition))), 2.5);
313
+ totalEmissiveRadiance += diffuseColor.rgb * (0.22 + 0.6 * fres);
314
+ #endif`,
315
+ )
316
+ .replace(
317
+ '#include <opaque_fragment>',
318
+ /* glsl */ `#ifdef TOON
319
+ float lum = dot(outgoingLight, vec3(0.299, 0.587, 0.114));
320
+ float ql = lum < 0.1 ? 0.55 : lum < 0.35 ? 0.8 : 1.0;
321
+ outgoingLight = diffuseColor.rgb * ql * 0.95 + totalEmissiveRadiance;
322
+ #ifdef SKETCH
323
+ float hs = sin((gl_FragCoord.x + gl_FragCoord.y) * 0.9);
324
+ float hs2 = sin((gl_FragCoord.x - gl_FragCoord.y) * 0.9);
325
+ float ink = (lum < 0.32 ? step(0.55, hs) : 0.0) + (lum < 0.14 ? step(0.55, hs2) : 0.0);
326
+ outgoingLight = mix(outgoingLight, vec3(0.12, 0.16, 0.22), clamp(ink, 0.0, 1.0) * 0.55);
327
+ #endif
328
+ #endif
329
+ #include <opaque_fragment>`,
330
+ );
331
+ };
332
+ return m;
333
+ };
334
+ return { base: make(false), shell: o.plush ? make(true) : null, uniforms };
335
+ }
336
+
337
+ /**
338
+ * Material que reutiliza la superficie del cuerpo desplazada a lo largo de la normal: contorno
339
+ * (casco invertido, BackSide) o parche recortado por una caja girada en XY (máscaras, tirita).
340
+ * Comparte los uniforms del cuerpo, así sigue sus deformaciones.
341
+ * @param {object} u uniforms del cuerpo
342
+ * @param {object} o { color, off, outline, box: {c:[x,y,z], h:[hx,hy,hz], rot}, kind, round }
343
+ */
344
+ export function surfaceMaterial(u, o) {
345
+ const outline = !!o.outline;
346
+ const m = outline
347
+ ? new THREE.MeshBasicMaterial({ color: o.color, side: THREE.BackSide })
348
+ : o.kind === 'plastic'
349
+ ? new THREE.MeshPhysicalMaterial({ color: o.color, roughness: 0.4, clearcoat: 0.4 })
350
+ : new THREE.MeshPhysicalMaterial({ color: o.color, roughness: 0.9, sheen: 0.8, sheenRoughness: 0.6, sheenColor: new THREE.Color(o.color).lerp(new THREE.Color('#fff'), 0.4) });
351
+ const box = o.box || { c: [0, 0, 0], h: [99, 99, 99], rot: 0 };
352
+ const own = {
353
+ uOff: { value: o.off || 0.01 },
354
+ uBoxC: { value: new THREE.Vector3(...box.c) },
355
+ uBoxH: { value: new THREE.Vector3(...box.h) },
356
+ uBoxR: { value: box.rot || 0 },
357
+ uRound: { value: o.round || 0 },
358
+ uWobble: { value: o.wobble || 0 },
359
+ };
360
+ m.customProgramCacheKey = () => `ots3d-surf-${outline ? 'line' : o.kind || 'felt'}`;
361
+ m.onBeforeCompile = (sh) => {
362
+ Object.assign(sh.uniforms, u, own);
363
+ const head = `uniform float uOff; uniform vec3 uBoxC; uniform vec3 uBoxH; uniform float uBoxR; uniform float uRound; uniform float uWobble;`;
364
+ sh.vertexShader = sh.vertexShader
365
+ .replace('#include <common>', `${VERT_HEAD}\n${head}`)
366
+ .replace(
367
+ '#include <begin_vertex>',
368
+ /* glsl */ `${VERT_BEGIN}
369
+ float wo = uWobble > 0.0 ? 1.0 + uWobble * (sin(position.x * 60.0 + position.y * 37.0) * 0.5 + 0.5) : 1.0;
370
+ transformed += dN * uOff * wo;`,
371
+ );
372
+ if (outline) {
373
+ sh.fragmentShader = sh.fragmentShader.replace('#include <common>', `#include <common>\n${COMMON}`);
374
+ return;
375
+ }
376
+ sh.fragmentShader = sh.fragmentShader
377
+ .replace('#include <common>', `#include <common>\n${COMMON}\n${head}`)
378
+ .replace(
379
+ '#include <clipping_planes_fragment>',
380
+ /* glsl */ `#include <clipping_planes_fragment>
381
+ {
382
+ vec3 d = vFurP - uBoxC;
383
+ float cr = cos(uBoxR), sr = sin(uBoxR);
384
+ d.xy = vec2(cr * d.x + sr * d.y, -sr * d.x + cr * d.y);
385
+ vec3 e = abs(d) - uBoxH + uRound;
386
+ if (length(max(e.xy, 0.0)) - uRound > 0.0 || e.z > uRound) discard;
387
+ }`,
388
+ );
389
+ };
390
+ return m;
391
+ }
392
+
393
+ // ── materiales pequeños compartidos (por color) ──
394
+ const small = new Map();
395
+ /** Material PBR compartido: kind = 'gloss'|'matte'|'metal'|'emissive'|'plastic'|'felt'|'glass'. */
396
+ export function mat(kind, color) {
397
+ const key = `${kind}|${color}`;
398
+ let m = small.get(key);
399
+ if (m) return m;
400
+ const c = new THREE.Color(color);
401
+ if (kind === 'gloss') m = new THREE.MeshPhysicalMaterial({ color: c, roughness: 0.12, clearcoat: 1, clearcoatRoughness: 0.03, envMapIntensity: 1.6 });
402
+ else if (kind === 'metal') m = new THREE.MeshStandardMaterial({ color: c, roughness: 0.25, metalness: 1, envMapIntensity: 1.4 });
403
+ else if (kind === 'emissive') m = new THREE.MeshStandardMaterial({ color: c, emissive: c, emissiveIntensity: 1.2, roughness: 0.4 });
404
+ else if (kind === 'felt') m = new THREE.MeshPhysicalMaterial({ color: c, roughness: 0.95, sheen: 1, sheenRoughness: 0.6, sheenColor: c.clone().lerp(new THREE.Color('#fff'), 0.4) });
405
+ else if (kind === 'plastic') m = new THREE.MeshPhysicalMaterial({ color: c, roughness: 0.35, clearcoat: 0.5, clearcoatRoughness: 0.2 });
406
+ else if (kind === 'glass') m = new THREE.MeshPhysicalMaterial({ color: c, roughness: 0.04, clearcoat: 1, transparent: true, opacity: 0.32, depthWrite: false, envMapIntensity: 1.6 });
407
+ else if (kind === 'toon') m = new THREE.MeshToonMaterial({ color: c });
408
+ else m = new THREE.MeshStandardMaterial({ color: c, roughness: 0.8 });
409
+ small.set(key, m);
410
+ return m;
411
+ }
412
+
413
+ let blobTex = null;
414
+ /** Textura de sombra de contacto (degradado radial), compartida. */
415
+ export function shadowTexture() {
416
+ if (blobTex) return blobTex;
417
+ const c = document.createElement('canvas');
418
+ c.width = c.height = 128;
419
+ const g = c.getContext('2d');
420
+ const gr = g.createRadialGradient(64, 64, 0, 64, 64, 64);
421
+ gr.addColorStop(0, 'rgba(0,0,0,0.55)');
422
+ gr.addColorStop(0.45, 'rgba(0,0,0,0.3)');
423
+ gr.addColorStop(1, 'rgba(0,0,0,0)');
424
+ g.fillStyle = gr;
425
+ g.fillRect(0, 0, 128, 128);
426
+ blobTex = new THREE.CanvasTexture(c);
427
+ blobTex.colorSpace = THREE.SRGBColorSpace;
428
+ return blobTex;
429
+ }
@@ -0,0 +1,187 @@
1
+ /**
2
+ * Escenario 3D compartido de 7ots: UN solo WebGLRenderer (un único canvas fijo que cubre la
3
+ * ventana, sin eventos de puntero) que dibuja todas las «vistas» de la página con scissor.
4
+ *
5
+ * Una vista = un elemento del DOM + su escena + su cámara + los personajes que contiene.
6
+ * `createCharacter3D(el, spec)` crea una vista con un personaje; `createCrowd3D(el)` crea una
7
+ * vista con muchos personajes en la misma escena (un solo render para todo el grupo).
8
+ *
9
+ * Rendimiento:
10
+ * - un contexto WebGL para toda la página (nunca uno por personaje);
11
+ * - devicePixelRatio con tope (`maxDpr`, 1.5 por defecto) y resolución adaptativa: si los FPS
12
+ * bajan de ~50 se reduce el DPR a pasos, y se recupera cuando sobra margen;
13
+ * - las vistas fuera de pantalla (IntersectionObserver) ni se animan ni se dibujan;
14
+ * - con la pestaña oculta el bucle se detiene del todo;
15
+ * - entorno (RoomEnvironment → PMREM) generado una vez y compartido por todas las escenas.
16
+ *
17
+ * Requisito: la página debe declarar el importmap de three (igual que src/avatar/AvatarStage.js):
18
+ * <script type="importmap">{ "imports": {
19
+ * "three": "https://cdn.jsdelivr.net/npm/three@0.170.0/build/three.module.js",
20
+ * "three/addons/": "https://cdn.jsdelivr.net/npm/three@0.170.0/examples/jsm/" } }</script>
21
+ */
22
+
23
+ import * as THREE from 'three';
24
+ import { RoomEnvironment } from 'three/addons/environments/RoomEnvironment.js';
25
+
26
+ const stages = new Map();
27
+
28
+ /**
29
+ * Escenario compartido (se crea la primera vez). `opts.key` separa escenarios: el widget usa
30
+ * el suyo ('overlay', con zIndex por encima de su panel) para no pelear con el de la página.
31
+ */
32
+ export function getStage(opts = {}) {
33
+ const k = opts.key || 'page';
34
+ if (!stages.has(k)) stages.set(k, new Stage(opts));
35
+ return stages.get(k);
36
+ }
37
+
38
+ class Stage {
39
+ constructor(opts) {
40
+ this.maxDpr = Math.min(opts.maxDpr || 1.5, window.devicePixelRatio || 1);
41
+ this.minDpr = Math.min(opts.minDpr || 0.75, this.maxDpr);
42
+ this.dpr = this.maxDpr;
43
+ this.adaptive = opts.adaptive !== false;
44
+ const canvas = document.createElement('canvas');
45
+ canvas.className = 'ots3d-canvas';
46
+ canvas.setAttribute('aria-hidden', 'true');
47
+ canvas.style.cssText = `position:fixed;left:0;top:0;width:100vw;height:100vh;pointer-events:none;z-index:${opts.zIndex ?? 1};display:block`;
48
+ (opts.parent || document.body).appendChild(canvas);
49
+ this.canvas = canvas;
50
+
51
+ const r = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: true, powerPreference: 'high-performance' });
52
+ r.setPixelRatio(this.dpr);
53
+ r.outputColorSpace = THREE.SRGBColorSpace;
54
+ r.toneMapping = THREE.NeutralToneMapping;
55
+ r.toneMappingExposure = 1.05;
56
+ r.setClearColor(0x000000, 0);
57
+ r.info.autoReset = false;
58
+ r.autoClear = false;
59
+ this.renderer = r;
60
+
61
+ const pm = new THREE.PMREMGenerator(r);
62
+ this.envMap = pm.fromScene(new RoomEnvironment(), 0.04).texture;
63
+ pm.dispose();
64
+
65
+ this.views = new Set();
66
+ this.visible = new Set();
67
+ this.raf = 0;
68
+ this.last = 0;
69
+ this.stats = { fps: 0, ms: 0, calls: 0, triangles: 0, dpr: this.dpr, views: 0, characters: 0 };
70
+ this._frames = 0;
71
+ this._acc = 0;
72
+ this._statT = 0;
73
+ this._good = 0;
74
+ this._w = 0;
75
+ this._h = 0;
76
+
77
+ this.io = new IntersectionObserver((entries) => {
78
+ for (const e of entries) {
79
+ const v = e.target.__ots3dView;
80
+ if (!v) continue;
81
+ if (e.isIntersecting) this.visible.add(v);
82
+ else this.visible.delete(v);
83
+ }
84
+ this.wake();
85
+ }, { rootMargin: '64px' });
86
+ this._vis = () => (document.hidden ? this.sleep() : this.wake());
87
+ document.addEventListener('visibilitychange', this._vis);
88
+ }
89
+
90
+ /** Escena estándar de estudio: entorno compartido + luz principal cálida, contraluz frío y relleno. */
91
+ makeScene() {
92
+ const scene = new THREE.Scene();
93
+ scene.environment = this.envMap;
94
+ scene.environmentIntensity = 0.75;
95
+ scene.add(new THREE.HemisphereLight(0xe8ecff, 0x40304e, 0.7));
96
+ const key = new THREE.DirectionalLight(0xfff1e0, 2.4);
97
+ key.position.set(2.2, 3.4, 3.2);
98
+ const rim = new THREE.DirectionalLight(0xcfe0ff, 2.2);
99
+ rim.position.set(-2.6, 2.2, -3.0);
100
+ const fill = new THREE.DirectionalLight(0xffe6f4, 0.5);
101
+ fill.position.set(-3, 0.5, 2);
102
+ scene.add(key, rim, fill);
103
+ return scene;
104
+ }
105
+
106
+ addView(view) {
107
+ this.views.add(view);
108
+ view.el.__ots3dView = view;
109
+ this.io.observe(view.el);
110
+ this.wake();
111
+ }
112
+
113
+ removeView(view) {
114
+ this.views.delete(view);
115
+ this.visible.delete(view);
116
+ this.io.unobserve(view.el);
117
+ delete view.el.__ots3dView;
118
+ }
119
+
120
+ wake() {
121
+ if (this.raf || document.hidden || !this.visible.size) return;
122
+ this.last = performance.now();
123
+ this.raf = requestAnimationFrame((t) => this.frame(t));
124
+ }
125
+
126
+ sleep() {
127
+ cancelAnimationFrame(this.raf);
128
+ this.raf = 0;
129
+ }
130
+
131
+ resize() {
132
+ const w = window.innerWidth;
133
+ const h = window.innerHeight;
134
+ const r = this.renderer;
135
+ if (w !== this._w || h !== this._h || r.getPixelRatio() !== this.dpr) {
136
+ this._w = w;
137
+ this._h = h;
138
+ r.setPixelRatio(this.dpr);
139
+ r.setSize(w, h, false);
140
+ }
141
+ }
142
+
143
+ frame(t) {
144
+ this.raf = 0;
145
+ if (document.hidden || !this.visible.size) return;
146
+ const dt = Math.min(64, Math.max(0, t - this.last));
147
+ this.last = t;
148
+ const t0 = performance.now();
149
+ this.resize();
150
+ const r = this.renderer;
151
+ r.info.reset();
152
+ r.setScissorTest(false);
153
+ r.clear();
154
+ r.setScissorTest(true);
155
+ let chars = 0;
156
+ for (const v of this.visible) {
157
+ const rect = v.el.getBoundingClientRect();
158
+ if (rect.bottom < 0 || rect.top > this._h || rect.right < 0 || rect.left > this._w || rect.width < 2 || rect.height < 2) continue;
159
+ v.rect = rect;
160
+ v.tick(t, dt);
161
+ chars += v.characters.length;
162
+ const bottom = this._h - rect.bottom;
163
+ r.setViewport(rect.left, bottom, rect.width, rect.height);
164
+ r.setScissor(rect.left, bottom, rect.width, rect.height);
165
+ r.render(v.scene, v.camera);
166
+ }
167
+ // estadísticas y resolución adaptativa
168
+ this._frames++;
169
+ this._acc += performance.now() - t0;
170
+ if (t - this._statT > 1000) {
171
+ const secs = (t - this._statT) / 1000;
172
+ const fps = this._statT ? this._frames / secs : 60;
173
+ Object.assign(this.stats, {
174
+ fps: Math.round(fps), ms: Math.round((this._acc / this._frames) * 10) / 10, calls: r.info.render.calls,
175
+ triangles: r.info.render.triangles, dpr: this.dpr, views: this.visible.size, characters: chars,
176
+ });
177
+ if (this.adaptive && this._statT) {
178
+ if (fps < 48 && this.dpr > this.minDpr) { this.dpr = Math.max(this.minDpr, Math.round((this.dpr - 0.25) * 100) / 100); this._good = 0; }
179
+ else if (fps > 57 && this.dpr < this.maxDpr && ++this._good >= 3) { this.dpr = Math.min(this.maxDpr, this.dpr + 0.25); this._good = 0; }
180
+ }
181
+ this._statT = t;
182
+ this._frames = 0;
183
+ this._acc = 0;
184
+ }
185
+ this.raf = requestAnimationFrame((tt) => this.frame(tt));
186
+ }
187
+ }