您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Attempts model download via runtime code modification. Enhanced geometry de-duplication. Requires JSZip/FileSaver.
当前为
// ==UserScript== // @name ncikkis sketchfab downloader // @namespace http://tampermonkey.net/ // @version 1.9 // Added content-based geometry de-duplication check // @description Attempts model download via runtime code modification. Enhanced geometry de-duplication. Requires JSZip/FileSaver. // @author ncikkis // @match https://sketchfab.com/* // @require https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js // @require https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/2.0.5/FileSaver.min.js // @run-at document-start // @grant unsafeWindow // @grant GM_download // @grant GM_xmlhttpRequest // @namespace https://gf.qytechs.cn/users/956968 // Original namespace kept for reference to technique origin // @license MIT // ==/UserScript== (async function() { 'use strict'; const LOG_PREFIX = '[ncikkis_SFD_v1.9]:'; // --- Global Storage --- let capturedGeometries = new Map(); // key: geomId, value: { name: string, objData: string } let capturedTextures = new Map(); // key: textureUrl, value: { name: string, url: string } let capturedGeometryKeys = new Set(); // Stores "vertexCount_indexCount" keys to prevent duplicates let geomCounter = 0; let textureCounter = 0; let downloadButtonAdded = false; console.log(`${LOG_PREFIX} Initializing. Waiting for Sketchfab JS...`); // --- Core Capture Functions (attached to window for injection) --- unsafeWindow.captureGeometry = function(geomObj) { // Basic filtering if (!geomObj || !geomObj._primitives || !geomObj._attributes?.Vertex?._elements) { return; } // --- Enhanced De-duplication --- // Check 1: Object reference flag (quick check) if(geomObj.__captured__) return; // Check 2: Content-based key (vertex count + index count) try { const vertexCount = geomObj._attributes.Vertex._elements.length; let totalIndexCount = 0; if (geomObj._primitives && Array.isArray(geomObj._primitives)) { geomObj._primitives.forEach(p => { if (p && p.indices?._elements) { totalIndexCount += p.indices._elements.length; } }); } // Only proceed if we have actual geometry if (vertexCount === 0 || totalIndexCount === 0) return; const geometryKey = `${vertexCount}_${totalIndexCount}`; if (capturedGeometryKeys.has(geometryKey)) { //console.log(`${LOG_PREFIX} Skipping duplicate geometry key: ${geometryKey}`); return; // Skip if key already processed } // Mark this key and object reference as captured capturedGeometryKeys.add(geometryKey); geomObj.__captured__ = true; // Mark object reference as well } catch (e) { console.error(`${LOG_PREFIX} Error during geometry de-duplication check:`, e, geomObj); // Fallback: mark object ref anyway to prevent potential infinite loops if error is persistent geomObj.__captured__ = true; return; // Don't process if key calculation failed } // --- End De-duplication --- geomCounter++; const geomId = `geom_${geomCounter}`; const modelName = `model_${geomCounter}`; console.log(`${LOG_PREFIX} Capturing NEW Geometry: ${modelName} (Key: ${capturedGeometryKeys.size})`, geomObj); try { const parsed = parseGeometry(geomObj); if (!parsed) { throw new Error("Parsing failed"); } const objStr = generateOBJ(parsed, modelName); capturedGeometries.set(geomId, { name: modelName, objData: objStr }); console.log(`${LOG_PREFIX} Stored OBJ data for ${modelName}`); } catch(e) { console.error(`${LOG_PREFIX} Error processing geometry ${modelName}:`, e); } }; unsafeWindow.captureTextureURL = function(imageInfo, imageModel) { // Same implementation as v1.8 if (!imageModel?.attributes?.images) { return imageInfo; } try { const originalUrl = imageInfo.url; const filename_image = imageModel.attributes.name || `texture_${textureCounter++}`; let bestUrl = originalUrl; let max_size = imageInfo.size || 0; imageModel.attributes.images.forEach(img => { if (img?.url && img.size > max_size) { max_size = img.size; bestUrl = img.url; } }); if (!capturedTextures.has(bestUrl)) { console.log(`${LOG_PREFIX} Capturing Texture URL: ${filename_image} -> ${bestUrl}`); capturedTextures.set(bestUrl, { name: filename_image, url: bestUrl }); } const bestImageInfo = imageModel.attributes.images.find(img => img.url === bestUrl) || imageInfo; return bestImageInfo; } catch (e) { console.error(`${LOG_PREFIX} Error in captureTextureURL:`, e); return imageInfo; } }; // --- Geometry Parsing and OBJ Generation --- function parseGeometry(geomObj) { // Same implementation as v1.8 const primitives = []; if (geomObj._primitives && Array.isArray(geomObj._primitives)) { geomObj._primitives.forEach(p => { if (p?.indices?._elements) { primitives.push({ mode: p.mode, indices: p.indices._elements }); } }); } const attr = geomObj._attributes; if (!attr?.Vertex?._elements) return null; let uvElements = []; for(let i=0; i<=8; i++) { if(attr[`TexCoord${i}`]?._elements) { uvElements = attr[`TexCoord${i}`]._elements; /* console.log(`${LOG_PREFIX} Using TexCoord${i}`); */ break; } } return { vertex: attr.Vertex._elements, normal: attr.Normal?._elements || [], uv: uvElements, primitives: primitives }; } function generateOBJ(parsedData, modelName) { // Same implementation as v1.8 let objStr = `# Generated by ncikkis Sketchfab Downloader v1.9\n`; objStr += `o ${modelName}\n`; for (let i = 0; i < parsedData.vertex.length; i += 3) { objStr += `v ${parsedData.vertex[i]} ${parsedData.vertex[i+1]} ${parsedData.vertex[i+2]}\n`; } const hasNormals = parsedData.normal.length > 0; if (hasNormals) { for (let i = 0; i < parsedData.normal.length; i += 3) { objStr += `vn ${parsedData.normal[i]} ${parsedData.normal[i+1]} ${parsedData.normal[i+2]}\n`; } } const hasUVs = parsedData.uv.length > 0; if (hasUVs) { for (let i = 0; i < parsedData.uv.length; i += 2) { objStr += `vt ${parsedData.uv[i]} ${1.0 - parsedData.uv[i+1]}\n`; } } objStr += `s 1\n`; for (const primitive of parsedData.primitives) { const indices = primitive.indices; if (primitive.mode === 4) { // TRIANGLES for (let i = 0; i < indices.length; i += 3) { objStr += `f`; for (let j = 0; j < 3; j++) { const idx = indices[i + j] + 1; objStr += ` ${idx}`; if (hasUVs || hasNormals) { objStr += `/`; if (hasUVs) objStr += idx; if (hasNormals) objStr += `/${idx}`; } } objStr += `\n`; } } else if (primitive.mode === 5) { // TRIANGLE_STRIP for (let i = 0; i + 2 < indices.length; i++) { objStr += `f`; const order = (i % 2 === 0) ? [0, 1, 2] : [0, 2, 1]; for (let j = 0; j < 3; j++) { const idx = indices[i + order[j]] + 1; objStr += ` ${idx}`; if (hasUVs || hasNormals) { objStr += `/`; if (hasUVs) objStr += idx; if (hasNormals) objStr += `/${idx}`; } } objStr += `\n`; } } else { console.warn(`${LOG_PREFIX} Unsupported primitive mode: ${primitive.mode}`); } } return objStr; } // --- Download Button and Packaging Logic --- function addDownloadButton() { // Same implementation as v1.8 if (downloadButtonAdded) return; const titleBar = document.querySelector('.titlebar') || document.querySelector('.viewer-header'); if (titleBar) { console.log(`${LOG_PREFIX} Adding download button...`); const btn = document.createElement("a"); btn.innerHTML = "DOWNLOAD ZIP"; /* Styles... */ btn.style.backgroundColor = "#1caad9"; btn.style.color = "white"; btn.style.padding = "8px"; btn.style.borderRadius = "4px"; btn.style.cursor = "pointer"; btn.style.marginLeft = "10px"; btn.style.textDecoration = "none"; btn.style.fontSize = "12px"; btn.style.fontWeight = "bold"; btn.onmouseover = () => btn.style.backgroundColor = "#1c88bb"; btn.onmouseout = () => btn.style.backgroundColor = "#1caad9"; btn.addEventListener("click", initiateDownloadPackage, false); titleBar.appendChild(btn); downloadButtonAdded = true; console.log(`${LOG_PREFIX} Download button added.`); } else { console.log(`${LOG_PREFIX} Title bar not found, retrying button add later...`); setTimeout(addDownloadButton, 2000); } } async function initiateDownloadPackage() { // Same implementation as v1.8 if (capturedGeometries.size === 0 && capturedTextures.size === 0) { alert("ncikkis Downloader: No geometry or textures captured."); return; } const zip = new JSZip(); const modelFolder = zip.folder('model'); console.log(`${LOG_PREFIX} Preparing download package...`); if (capturedGeometries.size > 0) { console.log(`${LOG_PREFIX} Adding ${capturedGeometries.size} geometry file(s) to zip...`); capturedGeometries.forEach((geomInfo) => { modelFolder.file(`${geomInfo.name}.obj`, geomInfo.objData); }); } else { console.warn(`${LOG_PREFIX} No geometry captured.`); } if (capturedTextures.size > 0) { console.log(`${LOG_PREFIX} Fetching ${capturedTextures.size} texture file(s)...`); const texturePromises = []; capturedTextures.forEach((texInfo) => { const promise = new Promise((resolve, reject) => { console.log(`${LOG_PREFIX} Requesting texture blob for: ${texInfo.name}`); GM_download({ url: texInfo.url, name: `temp_${texInfo.name}`, responseType: 'blob', onload: (response) => { if (response.response) { let safeName = texInfo.name.replace(/[^a-zA-Z0-9_.-]/g, '_'); if (!/\.(png|jpg|jpeg|webp)$/i.test(safeName)) safeName += '.png'; modelFolder.file(`textures/${safeName}`, response.response); resolve(); } else { console.error(`${LOG_PREFIX} GM_download success but no blob received for ${texInfo.name}`); reject(new Error(`No blob for ${texInfo.name}`)); } }, onerror: (err) => { console.error(`${LOG_PREFIX} GM_download error for ${texInfo.name} (${texInfo.url}):`, err); reject(err); }, ontimeout: () => { console.error(`${LOG_PREFIX} GM_download timeout for ${texInfo.name} (${texInfo.url})`); reject(new Error(`Timeout for ${texInfo.name}`)); } }); }); texturePromises.push(promise); }); try { await Promise.all(texturePromises); console.log(`${LOG_PREFIX} All texture downloads attempted.`); } catch (error) { console.error(`${LOG_PREFIX} Error(s) occurred during texture fetching:`, error); alert(`ncikkis Downloader: Error downloading textures. Zip may be incomplete.`); } } else { console.warn(`${LOG_PREFIX} No textures captured.`); } try { console.log(`${LOG_PREFIX} Generating zip file...`); const zipBlob = await zip.generateAsync({ type: "blob" }); let filename = "sketchfab_download.zip"; try { filename = document.querySelector('.model-name__label')?.textContent?.trim()?.replace(/[^a-zA-Z0-9_-]/g, '_') + ".zip" || filename; } catch (_) {} saveAs(zipBlob, filename); console.log(`${LOG_PREFIX} Zip file saving initiated: ${filename}`); } catch (e) { console.error(`${LOG_PREFIX} Error generating or saving zip file:`, e); alert(`ncikkis Downloader: Error generating zip file.`); } } // --- Script Interception and Patching --- // Same implementation as v1.8 const patchPoints = [ { regex: /(drawGeometry\(.*?,(.*?)\))/g, injection: (match, p1, p2) => `${p1}; window.captureGeometry(${p2});`, name: "Geometry Capture" }, { regex: /getResourceImage:function\((\w+),(\w+)\)\{/g, injection: (match, p1, p2) => `${match} ${p1} = window.captureTextureURL(${p1}, this._imageModel);`, name: "Texture URL Capture" } ]; function patchScript(scriptText, scriptUrl) { /* ... Same logic as v1.8 ... */ let modifiedText = scriptText; let patchesApplied = 0; console.log(`${LOG_PREFIX} Patching script: ${scriptUrl}`); patchPoints.forEach(patch => { let matchFound = false; let iteration = 0; const maxIterations = 1000; // Prevent infinite loops modifiedText = modifiedText.replace(patch.regex, (...args) => { matchFound = true; patchesApplied++; iteration++; if(iteration > maxIterations) throw new Error("Max iterations reached in replace"); const originalMatch = args[0]; if (typeof patch.injection === 'function') { return patch.injection(...args); } else { return originalMatch + patch.injection; } }); // Basic replace, might need /g flag handling if multiple replacements expected per regex if (matchFound) { console.log(`${LOG_PREFIX} Patch '${patch.name}' applied.`); } else { console.warn(`${LOG_PREFIX} Patch '${patch.name}' FAILED - pattern not found.`); } }); if (patchesApplied > 0) { console.log(`${LOG_PREFIX} Total patches applied: ${patchesApplied}.`); return modifiedText; } else { console.warn(`${LOG_PREFIX} No patches applied to script: ${scriptUrl}.`); return scriptText; } } (() => { /* ... Same MutationObserver/interception logic as v1.8 ... */ const observer = new MutationObserver((mutations) => { mutations.forEach((mutation) => { mutation.addedNodes.forEach((node) => { if (node.tagName === 'SCRIPT' && node.src && (node.src.includes('web/dist/') || node.src.includes('standaloneViewer') || node.src.includes('/viewer/'))) { node.async = false; node.defer = false; node.removeAttribute('integrity'); node.type = 'text/plain'; console.log(`${LOG_PREFIX} Intercepted Sketchfab script: ${node.src}`); GM_xmlhttpRequest({ method: "GET", url: node.src, onload: function(response) { if (response.status === 200) { console.log(`${LOG_PREFIX} Fetched script for patching.`); const patchedText = patchScript(response.responseText, node.src); const newScript = document.createElement('script'); newScript.type = "text/javascript"; newScript.textContent = patchedText; (document.head || document.documentElement).appendChild(newScript); console.log(`${LOG_PREFIX} Injected patched script.`); setTimeout(addDownloadButton, 1500); } else { console.error(`${LOG_PREFIX} Failed fetch script ${node.src}. Status: ${response.status}`); } }, onerror: function(error) { console.error(`${LOG_PREFIX} Error fetching script ${node.src}:`, error); } }); node.remove(); } }); }); }); observer.observe(document, { childList: true, subtree: true }); console.log(`${LOG_PREFIX} MutationObserver active.`); setTimeout(addDownloadButton, 7000); // Fallback button add })(); })(); // End of IIFE
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址