// ==UserScript==
// @name Sketchfab Model Downloader
// @version 1
// @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;
const func_drawGeometry = /(this\._stateCache\.drawGeometry\(this\._graphicContext,t\))/g;
const func_drawArrays = /t\.drawArrays\(t\.TRIANGLES,0,6\)/g;
const func_renderInto1 = /A\.renderInto\(n,E,R/g;
const func_renderInto2 = /g\.renderInto=function\(e,i,r/g;
const func_getResourceImage= /getResourceImage:function\(e,t\){/g;
let zip, folder;
let objects = {};
let saveimagecache2 = {};
let button_dw = false;
window.allmodel = [];
function resetDownloadState() {
zip = new JSZip();
folder = zip.folder('collection');
objects = {};
saveimagecache2 = {};
}
function addbtnfunc() {
const titlebar = document.evaluate("//div[@class='titlebar']", document, null, 9, null).singleNodeValue;
if (titlebar && !button_dw) {
const btn = document.createElement("a");
btn.className = "control";
btn.innerHTML = `
<pre style="
background-color: #e74c3c;
color: white;
padding: 10px 20px;
border-radius: 8px;
font-size: 16px;
cursor: pointer;
margin: 4px 2px;
display: inline-block;
text-align: center;
">DOWNLOAD</pre>`;
btn.addEventListener("click", dodownload, false);
titlebar.appendChild(btn);
button_dw = true;
} else if (!button_dw) {
setTimeout(addbtnfunc, 3000);
}
}
function parseobj(obj) {
const primitives = [];
obj._primitives.forEach(p => {
if (p && p.indices) {
primitives.push({
mode: p.mode,
indices: p.indices._elements
});
}
});
const attr = obj._attributes;
const uv = (['TexCoord0','TexCoord1','TexCoord2','TexCoord3','TexCoord4','TexCoord5','TexCoord6','TexCoord7','TexCoord8']
.find(ch => attr[ch])) || null;
return {
vertex: attr.Vertex._elements,
normal: attr.Normal ? attr.Normal._elements : [],
uv: uv ? attr[uv]._elements : [],
primitives: primitives
};
}
function dosavefile(mdl) {
const { name, obj } = mdl;
let str = '';
str += `mtllib ${name}.mtl\n`;
str += `o ${name}\n`;
for (let i = 0; i < obj.vertex.length; i += 3) {
str += `v ${obj.vertex[i]} ${obj.vertex[i+1]} ${obj.vertex[i+2]}\n`;
}
for (let i = 0; i < obj.normal.length; i += 3) {
str += `vn ${obj.normal[i]} ${obj.normal[i+1]} ${obj.normal[i+2]}\n`;
}
for (let i = 0; i < obj.uv.length; i += 2) {
str += `vt ${obj.uv[i]} ${obj.uv[i+1]}\n`;
}
str += `s on\n`;
const hasVN = obj.normal.length > 0;
const hasVT = obj.uv.length > 0;
obj.primitives.forEach(primitive => {
if (primitive.mode === 4 || primitive.mode === 5) {
const isStrip = primitive.mode === 5;
for (let j = 0; j + 2 < primitive.indices.length; isStrip ? j++ : j += 3) {
str += 'f ';
let order = [0,1,2];
if (isStrip && (j % 2 === 1)) order = [0,2,1];
for (let k = 0; k < 3; k++) {
const idx = primitive.indices[j + order[k]] + 1;
str += idx;
if (hasVT || hasVN) {
str += '/';
if (hasVT) str += idx;
if (hasVN) str += `/${idx}`;
}
str += ' ';
}
str += '\n';
}
}
});
objects[`${name}.obj`] = new Blob([str], { type: 'text/plain' });
}
function PackAll() {
for (const filename in objects) {
folder.file(filename, objects[filename], { binary: true });
}
const modelName = document.querySelector('.model-name__label')?.textContent.trim() || 'sketchfab';
zip.generateAsync({ type: 'blob' })
.then(content => saveAs(content, `${modelName}.zip`));
}
function dodownload() {
resetDownloadState();
window.allmodel.forEach((obj, idx) => {
const mdl = {
name: `model_${idx}`,
obj: parseobj(obj)
};
dosavefile(mdl);
});
PackAll();
}
window.attachbody = function(obj) {
if (!obj._faked && (obj._name || (obj.stateset && obj.stateset._name))) {
obj._faked = true;
if (obj._name === "composer layer" || obj._name === "Ground - Geometry") return;
window.allmodel.push(obj);
}
};
window.drawhookcanvas = function(e, imagemodel) {
if (!imagemodel) return e;
const format = e.options.format;
let best = { size:0 }, bestUrl = e.url;
imagemodel.attributes.images.forEach(img => {
const okFormat = format==='A' ? img.options.format==='A' : true;
if (okFormat && img.size > best.size) {
best = img;
bestUrl = img.url;
}
});
if (!saveimagecache2[bestUrl]) {
saveimagecache2[bestUrl] = { name: imagemodel.attributes.name };
}
return best;
};
window.drawhookimg = function(gl, t) {
const url = t[5].currentSrc;
if (!saveimagecache2[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 rowBytes = w*4;
for (let y=0; y<Math.floor(h/2); y++) {
const topOff = y*rowBytes, botOff = (h-y-1)*rowBytes;
const tmp = px.slice(topOff, topOff+rowBytes);
px.copyWithin(topOff, botOff, botOff+rowBytes);
px.set(tmp, botOff);
}
const canvas = document.createElement('canvas');
canvas.width = w; canvas.height = h;
const ctx = canvas.getContext('2d');
const imgData = ctx.createImageData(w,h);
imgData.data.set(px);
ctx.putImageData(imgData, 0, 0);
const baseName = saveimagecache2[url].name.replace(/\.[^\.]+$/, '');
canvas.toBlob(blob => {
objects[`${baseName}.png`] = blob;
}, 'image/png');
};
(() => {
class BeforeEvent {
constructor(script) {
this.script = script;
this._cancel = false;
}
preventDefault() { this._cancel = true; }
}
const cbs = [];
window.addBeforeScriptExecuteListener = f => {
if (typeof f!=='function') throw new Error("Handler must be function");
cbs.push(f);
};
const dispatch = node => {
if (node.tagName!=='SCRIPT') return;
const e = new BeforeEvent(node);
cbs.forEach(fn=>fn(e));
if (e._cancel) node.remove();
};
new MutationObserver(muts => {
muts.forEach(m =>
m.addedNodes.forEach(dispatch)
);
}).observe(document, { childList:true, subtree:true });
})();
window.onbeforescriptexecute = e => {
const src = e.script?.src||"";
if (!/web\/dist\/|standaloneViewer/.test(src)) return;
e.preventDefault();
const req = new XMLHttpRequest();
req.open('GET', src, false);
req.send();
let txt = req.responseText;
txt = txt.replace(func_renderInto1, "$& ,i");
txt = txt.replace(func_renderInto2, "$& ,image_data");
txt = txt.replace(func_drawArrays, "$& ,window.drawhookimg(t,image_data)");
txt = txt.replace(func_getResourceImage, "$& e = window.drawhookcanvas(e,this._imageModel);");
txt = txt.replace(func_drawGeometry, (m, g1) => g1 + ";window.attachbody(t);");
const s2 = document.createElement('script');
s2.type = "text/javascript";
s2.text = txt;
document.head.appendChild(s2);
setTimeout(addbtnfunc, 3000);
};
})();