PageSourceSearch

https://vrcmods.com/js/item-media-viewer/three-helpers.js?viewer=materials-v45

js vrcmods.com collected 2026-10-01 12:09:31 UTC 7,250 bytes, 200 lines download raw bytes

1import * as THREE from "three";
2
3export function frameObject(object, camera, controls) {
4  const box = computePreviewBox(object);
5  if (box.isEmpty()) return;
6  const center = box.getCenter(new THREE.Vector3());
7  const size = box.getSize(new THREE.Vector3());
8  const vFov = THREE.MathUtils.degToRad(camera.fov);
9  const hFov = 2 * Math.atan(Math.tan(vFov / 2) * (camera.aspect || 1));
10  const tanV = Math.tan(vFov / 2);
11  const tanH = Math.tan(hFov / 2);
12  const direction = new THREE.Vector3(1, 0.75, 1).normalize();
13  const corners = [
14    new THREE.Vector3(box.min.x, box.min.y, box.min.z),
15    new THREE.Vector3(box.min.x, box.min.y, box.max.z),
16    new THREE.Vector3(box.min.x, box.max.y, box.min.z),
17    new THREE.Vector3(box.min.x, box.max.y, box.max.z),
18    new THREE.Vector3(box.max.x, box.min.y, box.min.z),
19    new THREE.Vector3(box.max.x, box.min.y, box.max.z),
20    new THREE.Vector3(box.max.x, box.max.y, box.min.z),
21    new THREE.Vector3(box.max.x, box.max.y, box.max.z),
22  ];
23  let distance = Math.max(size.x, size.y, size.z, 0.35);
24  for (let pass = 0; pass < 4; pass += 1) {
25    camera.position.copy(center).addScaledVector(direction, distance);
26    camera.lookAt(center);
27    camera.updateMatrixWorld(true);
28    let extra = 0;
29    for (const corner of corners) {
30      const p = corner.clone().applyMatrix4(camera.matrixWorldInverse);
31      const neededDepth = Math.max(Math.abs(p.x) / Math.max(tanH, 0.001), Math.abs(p.y) / Math.max(tanV, 0.001));
32      extra = Math.max(extra, neededDepth - (-p.z));
33    }
34    if (extra <= 0.0001) break;
35    distance += extra;
36  }
37  distance *= 1.14;
38  controls.target.copy(center);
39  camera.position.copy(center).addScaledVector(direction, distance);
40  camera.lookAt(center);
41  camera.near = Math.max(distance / 1000, 0.001);
42  camera.far = Math.max(distance * 100, 2000);
43  camera.updateProjectionMatrix();
44  controls.update();
45}
46
47export function computePreviewBox(object) {
48  object.updateMatrixWorld(true);
49  const box = new THREE.Box3();
50  object.traverse((node) => {
51    if (!node.isMesh) return;
52    node.frustumCulled = false;
53    if (node.geometry) {
54      if (!node.geometry.boundingBox) node.geometry.computeBoundingBox();
55      if (!node.geometry.boundingSphere) node.geometry.computeBoundingSphere();
56      if (node.geometry.boundingBox) {
57        box.union(node.geometry.boundingBox.clone().applyMatrix4(node.matrixWorld));
58      }
59    }
60  });
61  return box.isEmpty() ? new THREE.Box3().setFromObject(object) : box;
62}
63
64export function colorLuminance(color) {
65  if (!color) return 1;
66  return (color.r * 0.2126) + (color.g * 0.7152) + (color.b * 0.0722);
67}
68
69export function textureHasImage(texture) {
70  if (!texture) return false;
71  const image = texture.image || texture.source?.data;
72  if (!image) return false;
73  if (image.data) return true;
74  if (typeof image.naturalWidth === "number") return image.naturalWidth > 0;
75  if (typeof image.videoWidth === "number") return image.videoWidth > 0;
76  return (image.width || 0) > 0 && (image.height || 0) > 0;
77}
78
79export function prepareTexture(texture, colorTexture) {
80  if (!texture || !textureHasImage(texture)) return null;
81  if (colorTexture) texture.colorSpace = THREE.SRGBColorSpace;
82  texture.anisotropy = 4;
83  texture.needsUpdate = true;
84  return texture;
85}
86
87function usableUvAttribute(attribute) {
88  if (!attribute || !attribute.count || attribute.count < 3) return false;
89  const sampleCount = Math.min(attribute.count, 512);
90  const step = Math.max(1, Math.floor(attribute.count / sampleCount));
91  let seen = 0;
92  let minU = Infinity, minV = Infinity, maxU = -Infinity, maxV = -Infinity;
93  for (let i = 0; i < attribute.count; i += step) {
94    const u = attribute.getX(i);
95    const v = attribute.getY(i);
96    if (!Number.isFinite(u) || !Number.isFinite(v)) continue;
97    seen += 1;
98    minU = Math.min(minU, u);
99    minV = Math.min(minV, v);
100    maxU = Math.max(maxU, u);
101    maxV = Math.max(maxV, v);
102  }
103  return seen >= 3 && (Math.abs(maxU - minU) > 0.00001 || Math.abs(maxV - minV) > 0.00001);
104}
105
106function generatedPlanarUvs(geometry) {
107  const position = geometry?.attributes?.position;
108  if (!position || !position.count) return null;
109  geometry.computeBoundingBox();
110  const box = geometry.boundingBox;
111  if (!box || box.isEmpty()) return null;
112  const size = box.getSize(new THREE.Vector3());
113  const axes = [
114    { key: "x", size: Math.abs(size.x) },
115    { key: "y", size: Math.abs(size.y) },
116    { key: "z", size: Math.abs(size.z) },
117  ].sort((a, b) => b.size - a.size);
118  const uAxis = axes[0].key;
119  const vAxis = axes[1].key;
120  const uSize = axes[0].size || 1;
121  const vSize = axes[1].size || 1;
122  const uMin = box.min[uAxis];
123  const vMin = box.min[vAxis];
124  const values = new Float32Array(position.count * 2);
125  const p = new THREE.Vector3();
126  for (let i = 0; i < position.count; i += 1) {
127    p.fromBufferAttribute(position, i);
128    values[i * 2] = (p[uAxis] - uMin) / uSize;
129    values[i * 2 + 1] = (p[vAxis] - vMin) / vSize;
130  }
131  return new THREE.BufferAttribute(values, 2);
132}
133
134export function ensureTextureUvAttributes(geometry, { needsUv = true, needsUv2 = false, allowPlanarProjection = false } = {}) {
135  const result = { primary: "none", secondary: "none" };
136  if (!geometry?.attributes) return result;
137
138  if (needsUv) {
139    if (usableUvAttribute(geometry.attributes.uv)) {
140      result.primary = "existing";
141    } else if (usableUvAttribute(geometry.attributes.uv2)) {
142      geometry.setAttribute("uv", geometry.attributes.uv2.clone());
143      result.primary = "copied-uv2";
144    } else if (allowPlanarProjection) {
145      const generated = generatedPlanarUvs(geometry);
146      if (generated) {
147        geometry.setAttribute("uv", generated);
148        result.primary = "generated-planar";
149      }
150    }
151  }
152
153  if (needsUv2) {
154    if (usableUvAttribute(geometry.attributes.uv2)) {
155      result.secondary = "existing";
156    } else if (usableUvAttribute(geometry.attributes.uv)) {
157      geometry.setAttribute("uv2", geometry.attributes.uv.clone());
158      result.secondary = "copied-uv";
159    }
160  }
161
162  return result;
163}
164
165export function computeModelStats(object) {
166  let meshes = 0, tris = 0, vertices = 0, bones = 0, skinned = 0;
167  const materials = new Map();
168  object.traverse((node) => {
169    if (node.isBone) bones += 1;
170    if (!node.isMesh || !node.geometry) return;
171    meshes += 1;
172    if (node.isSkinnedMesh) skinned += 1;
173    const g = node.geometry;
174    const count = g.index ? g.index.count : (g.attributes.position ? g.attributes.position.count : 0);
175    if (g.index) tris += count / 3;
176    else tris += count / 3;
177    if (g.attributes.position) vertices += g.attributes.position.count;
178    const mats = Array.isArray(node.material) ? node.material : [node.material];
179    mats.forEach((m) => {
180      if (!m) return;
181      const key = m.uuid;
182      if (!materials.has(key)) {
183        materials.set(key, {
184          name: m.name || "(unnamed)",
185          color: m.userData?.previewColor || (m.color ? `#${m.color.getHexString()}` : "#888"),
186          hasMap: !!m.map,
187        });
188      }
189    });
190  });
191  return {
192    meshes,
193    triangles: Math.round(tris),
194    vertices,
195    materials: Array.from(materials.values()),
196    materialCount: materials.size,
197    bones,
198    skinnedMeshes: skinned,
199  };
200}

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.