您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Download Sketchfab models
// ==UserScript== // @name Sketchfab Model Downloader // @version 1.3 // @description Download Sketchfab models // @author ncikkis // @include /^https?:\/\/(www\.)?sketchfab\.com\/.*/ // @require https://cdnjs.cloudflare.com/ajax/libs/jszip/3.1.5/jszip.min.js // @require https://cdnjs.cloudflare.com/ajax/libs/jszip-utils/0.0.2/jszip-utils.min.js // @require https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/1.3.8/FileSaver.js // @run-at document-start // @grant unsafeWindow // @namespace https://gf.qytechs.cn/users/1465318 // ==/UserScript== (function(){ 'use strict'; const window = unsafeWindow; if (typeof saveAs === 'function') window.saveAs = saveAs; const rDrawGeom = /(this\._stateCache\.drawGeometry\(this\._graphicContext,t\))/g; const rDrawArrays = /t\.drawArrays\(t\.TRIANGLES,0,6\)/g; const rRend1 = /A\.renderInto\(n,E,R/g; const rRend2 = /g\.renderInto=function\(e,i,r/g; const rGetResImg = /getResourceImage:function\(e,t\){/g; let zip, folder, objects, cache, buttonInjected; window.allmodel = []; function reset() { zip = new JSZip(); folder = zip.folder('collection'); objects = {}; cache = {}; } function tryInsertButton(){ const titleSpan = document.querySelector('span.model-name__label'); if (titleSpan && !buttonInjected) { const btn = document.createElement('button'); btn.textContent = 'Download this model'; btn.style.cssText = [ 'background:#e74c3c', 'color:#fff', 'padding:10px 20px', 'border:none', 'border-radius:8px', 'font-size:16px', 'cursor:pointer', 'margin-bottom:8px', 'display:block' ].join(';'); btn.addEventListener('click', downloadAll); titleSpan.parentNode.insertBefore(btn, titleSpan); buttonInjected = true; } else if (!buttonInjected) { setTimeout(tryInsertButton, 3000); } } function parseMesh(o){ const prim = []; o._primitives.forEach(p => { if (p && p.indices) prim.push({ mode: p.mode, indices: p.indices._elements }); }); const A = o._attributes; const uvKey = ['TexCoord0','TexCoord1','TexCoord2','TexCoord3','TexCoord4','TexCoord5','TexCoord6','TexCoord7','TexCoord8'] .find(k => A[k]); return { vertex: A.Vertex._elements, normal: A.Normal ? A.Normal._elements : [], uv: uvKey ? A[uvKey]._elements : [], primitives: prim }; } function saveMesh(m){ let s = `mtllib ${m.name}.mtl\no ${m.name}\n`; m.obj.vertex.forEach((v,i) => { if(i%3===0) s += `v ${v} ${m.obj.vertex[i+1]} ${m.obj.vertex[i+2]}\n`; }); m.obj.normal.forEach((v,i) => { if(i%3===0) s += `vn ${v} ${m.obj.normal[i+1]} ${m.obj.normal[i+2]}\n`; }); m.obj.uv.forEach((v,i) => { if(i%2===0) s += `vt ${v} ${m.obj.uv[i+1]}\n`; }); s += 's on\n'; const hasVT = m.obj.uv.length > 0, hasVN = m.obj.normal.length > 0; m.obj.primitives.forEach(p => { if (p.mode === 4 || p.mode === 5) { const strip = p.mode === 5; for (let j = 0; j + 2 < p.indices.length; strip ? j++ : j += 3) { s += 'f '; let order = [0,1,2]; if (strip && (j % 2)) order = [0,2,1]; order.forEach(o => { const idx = p.indices[j+o] + 1; s += idx; if (hasVT || hasVN) { s += '/'; if (hasVT) s += idx; if (hasVN) s += `/${idx}`; } s += ' '; }); s += '\n'; } } }); objects[`${m.name}.obj`] = new Blob([s], { type: 'text/plain' }); } function packageAll(){ Object.keys(objects).forEach(fn => folder.file(fn, objects[fn], { binary: true })); const title = document.querySelector('span.model-name__label')?.textContent.trim() || 'sketchfab'; zip.generateAsync({ type: 'blob' }).then(b => saveAs(b, `${title}.zip`)); } function downloadAll(){ reset(); window.allmodel.forEach((o,i) => saveMesh({ name: `model_${i}`, obj: parseMesh(o) })); packageAll(); } window.attachbody = o => { if (!o._faked && (o._name || (o.stateset && o.stateset._name))) { o._faked = true; if (o._name !== "composer layer" && o._name !== "Ground - Geometry") { window.allmodel.push(o); } } }; window.drawhookcanvas = (e, mod) => { if (!mod) return e; let best = { size: 0 }, url = e.url; mod.attributes.images.forEach(img => { const ok = e.options.format === 'A' ? img.options.format === 'A' : true; if (ok && img.size > best.size) { best = img; url = img.url; } }); if (!cache[url]) cache[url] = { name: mod.attributes.name }; return best; }; window.drawhookimg = (gl, t) => { const url = t[5].currentSrc; if (!cache[url]) return; const w = t[5].width, h = t[5].height; const px = new Uint8Array(w*h*4); gl.readPixels(0,0,w,h,gl.RGBA,gl.UNSIGNED_BYTE,px); const row = w*4; for (let y = 0; y < h/2; y++) { const top = y*row, bot = (h-y-1)*row; const tmp = px.slice(top, top+row); px.copyWithin(top, bot, bot+row); px.set(tmp, bot); } const c = document.createElement('canvas'); c.width = w; c.height = h; const cx = c.getContext('2d'), id = cx.createImageData(w,h); id.data.set(px); cx.putImageData(id, 0, 0); const bn = cache[url].name.replace(/\.[^.]+$/, ''); c.toBlob(b => { objects[`${bn}.png`] = b; }, 'image/png'); }; ;(function(){ class E{ constructor(s){ this.script=s; this._cancel=false; } preventDefault(){ this._cancel=true; } } const cbs = []; window.addBeforeScriptExecuteListener = f => { if (typeof f !== 'function') throw ''; cbs.push(f); }; const disp = n => { if (n.tagName === 'SCRIPT') { const ev = new E(n); cbs.forEach(f => f(ev)); if (ev._cancel) n.remove(); } }; new MutationObserver(ms => ms.forEach(m => m.addedNodes.forEach(disp))) .observe(document, { childList: true, subtree: true }); })(); window.onbeforescriptexecute = e => { const s = e.script?.src || ''; if (!/web\/dist\/|standaloneViewer/.test(s)) return; e.preventDefault(); const r = new XMLHttpRequest(); r.open('GET', s, false); r.send(); let t = r.responseText; t = t.replace(rRend1, "$& ,i") .replace(rRend2, "$& ,image_data") .replace(rDrawArrays, "$& ,window.drawhookimg(t,image_data)") .replace(rGetResImg, "$& e=window.drawhookcanvas(e,this._imageModel);") .replace(rDrawGeom, (m, g) => g + ";window.attachbody(t);"); const s2 = document.createElement('script'); s2.text = t; document.head.appendChild(s2); setTimeout(tryInsertButton, 3000); }; })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址