Quixel Megascans 유료화 대비! 무료 에셋 한 번에 다운로드 받는 크롬 개발자 도구 스크립트 완벽 가이드
안녕하세요, 크리에이터 여러분! 최근 Quixel Megascans의 유료화 소식이 들려오면서, 그동안 무료로 제공되었던 방대한 에셋들을 미리 다운받아야 하는 상황이 되었습니다. Quixel은 많은 3D 아티스트와 게임 개발자들이 애용해온 플랫폼이기 때문에, 이번 유료화 결정으로 인해 무료 에셋들을 빠르게 받아야 할 필요성이 생겼죠.다행히 vanthunder라는 개발자가 크롬 브라우저의 개발자 도구를 사용해 Quixel의 무료 에셋을 한 번에 다운로드할 수 있는 스크립트를 만들어 공유해주었습니다. 이 스크립트를 활용하면 브라우저 상에서 손쉽게 수십, 수백 개의 무료 에셋을 자동으로 다운로드할 수 있습니다.
이제, 하나씩 클릭해서 다운로드할 필요 없이 크롬 개발자 도구만으로 무료 에셋들을 한꺼번에 다운로드할 수 있는 방법을 알려드릴게요!
2025년부터 퀵셀의 메가스캔 데이터는 더 이상 무료로 사용하기 어려울 것으로 예상됩니다. 정확히 부분 유료화인지 전면 유료화인지는 공식 발표가 있어야 알 수 있지만, 이 변화에 대비하는 것이 중요합니다. 이번 포스팅에서는 메가스캔 에셋들을 빠르고 효율적으로 구매하는 방법과 이에 따른 에러 해결 방법을 소개하겠습니다.
1. 메가스캔 데이터 구매 준비
먼저 퀵셀 메가스캔 컬렉션 페이지로 이동하여 로그인합니다. 현재 약 18,876개의 에셋이 등록되어 있으며, 이 모든 에셋을 수작업으로 구매하는 것은 매우 비효율적입니다. 그래서 이를 한 번에 자동으로 구매하는 방법을 소개합니다.
2. 개발자 도구 사용하기
에셋을 빠르게 구매하려면 웹 브라우저의 개발자 도구를 사용합니다. F12 키를 눌러 개발자 도구를 열고, 콘솔 탭으로 이동합니다.
(async (startPage = 0, autoClearConsole = true) => {
const getCookie = (name) => {
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);
if (parts.length === 2) return parts.pop().split(';').shift();
};
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const fetchWithTimeout = (resource, options = {}) => {
const { timeout = 10000 } = options;
return Promise.race([
fetch(resource, options),
new Promise((_, reject) =>
setTimeout(() => reject(new Error('Request timeout')), timeout)
),
]);
};
const callCacheApi = async (params = {}) => {
const defaultParams = {
page: 0,
maxValuesPerFacet: 1000,
hitsPerPage: 1000,
attributesToRetrieve: ["id", "name"].join(","),
};
const fetchData = async () => {
const response = await fetchWithTimeout("https://proxy-algolia-prod.quixel.com/algolia/cache", {
headers: {
"x-api-key": "2Zg8!d2WAHIUW?pCO28cVjfOt9seOWPx@2j",
},
body: JSON.stringify({
url: "https://6UJ1I5A072-2.algolianet.com/1/indexes/assets/query?x-algolia-application-id=6UJ1I5A072&x-algolia-api-key=e93907f4f65fb1d9f813957bdc344892",
params: new URLSearchParams({ ...defaultParams, ...params }).toString(),
}),
method: "POST",
});
if (!response.ok) {
throw new Error(`Error fetching from Cache API: ${response.statusText}`);
}
return await response.json();
};
return await retryOperation(fetchData, 2000, 5);
};
const callAcl = async ({ id, name }) => {
const fetchData = async () => {
const response = await fetchWithTimeout("https://quixel.com/v1/acl", {
headers: {
authorization: "Bearer " + authToken,
"content-type": "application/json;charset=UTF-8",
},
body: JSON.stringify({ assetID: id }),
method: "POST",
});
if (!response.ok) {
throw new Error(`Error adding item ${id} | ${name}: ${response.statusText}`);
}
const json = await response.json();
if (json?.isError) {
console.error(` --> **Failed to add item** Item ${id} | ${name} (${json?.msg})`);
} else {
console.log(` --> Added item ${id} | ${name}`);
}
};
return await retryOperation(fetchData, 2000, 5);
};
const callAcquired = async () => {
const fetchData = async () => {
const response = await fetchWithTimeout("https://quixel.com/v1/assets/acquired", {
headers: {
authorization: "Bearer " + authToken,
"content-type": "application/json;charset=UTF-8",
},
method: "GET",
});
if (!response.ok) {
throw new Error(`Error fetching acquired items: ${response.statusText}`);
}
return await response.json();
};
return await retryOperation(fetchData, 2000, 5);
};
const retryOperation = async (operation, delay, retries) => {
let lastError;
for (let attempt = 1; attempt <= retries; attempt++) {
try {
return await operation();
} catch (error) {
lastError = error;
console.warn(`Attempt ${attempt} failed (${error.message}). Retrying in ${delay}ms...`);
await sleep(delay);
delay *= 2; // Exponential backoff
}
}
throw lastError;
};
let authToken = "";
const initialize = async () => {
console.log("-> Checking Auth API Token...");
try {
const authCookie = getCookie("auth") ?? "{}";
authToken = JSON.parse(decodeURIComponent(authCookie))?.token;
if (!authToken) {
throw new Error("-> Error: Authentication token not found. Please log in again.");
}
} catch (_) {
throw new Error("-> Error: Authentication token not found. Please log in again.");
}
console.log("-> Fetching acquired items...");
acquiredItems = (await callAcquired()).map((a) => a.assetID);
console.log("-> Fetching total number of pages...");
const initialData = await callCacheApi();
totalPages = initialData.nbPages;
itemsPerPage = initialData.hitsPerPage;
totalItems = initialData.nbHits;
console.log("-> ==============================================");
console.log(`-> Total items: ${totalItems}`);
console.log(`-> ${totalPages} total pages with ${itemsPerPage} items per page`);
console.log(`-> Total items to add: ${totalItems - acquiredItems.length}.`);
console.log("-> ==============================================");
if (!confirm(`Click OK to add ${totalItems - acquiredItems.length} items to your account.`)) {
throw new Error("-> Process cancelled by user.");
}
};
let acquiredItems = [];
let totalPages = 0;
let itemsPerPage = 0;
let totalItems = 0;
const MAX_CONCURRENT_REQUESTS = 5;
const mainProcess = async () => {
for (let pageIdx = startPage || 0; pageIdx < totalPages; pageIdx++) {
console.log(`-> ======================= PAGE ${pageIdx + 1}/${totalPages} START =======================`);
console.log("-> Fetching items from page " + (pageIdx + 1) + " ...");
const pageData = await callCacheApi({ page: pageIdx });
const items = pageData.hits;
console.log("-> Adding unacquired items...");
// Filter out already acquired items
const unownedItems = items.filter((i) => !acquiredItems.includes(i.id));
// Save current progress in localStorage
localStorage.setItem('currentPage', pageIdx);
// Limit concurrent requests
const queue = [...unownedItems];
const workers = Array.from({ length: MAX_CONCURRENT_REQUESTS }, async () => {
while (queue.length > 0) {
const item = queue.shift();
try {
await callAcl(item);
} catch (error) {
console.error(`Error with item ${item.id}: ${error.message}`);
}
}
});
await Promise.all(workers);
console.log(`-> ======================= PAGE ${pageIdx + 1}/${totalPages} COMPLETED =======================`);
if (autoClearConsole) console.clear();
}
};
const finalize = async () => {
console.log("-> Fetching new acquisition info...");
const newAcquiredItems = await callAcquired();
const newItemsAcquired = newAcquiredItems.length;
const newTotalCount = (await callCacheApi()).nbHits;
console.log(`-> Completed. Your account now has a total of ${newItemsAcquired} out of ${newTotalCount} items.`);
alert(`-> Your account now has a total of ${newItemsAcquired} out of ${newTotalCount} items.\n\nIf you find some items missing, try refreshing the page and run the script again.`);
};
try {
// Check if progress was saved
const savedPage = localStorage.getItem('currentPage');
if (savedPage !== null) {
startPage = parseInt(savedPage, 10);
console.log(`-> Resuming from page ${startPage + 1}`);
}
await initialize();
await mainProcess();
await finalize();
// Clear progress
localStorage.removeItem('currentPage');
} catch (error) {
console.error(error.message);
console.log("-> The script could not be completed.");
}
})();
깃허브에 올라온 스크립트를 복사하여 콘솔에 붙여넣습니다. 기본 스크립트는 API 제한 문제로 에러가 발생할 수 있으므로, 커뮤니티에서 수정된 스크립트를 사용하는 것이 좋습니다.
3. 스크립트 실행 및 에셋 구매
수정된 스크립트를 콘솔에 붙여넣고 실행하면, 모든 에셋을 빠르게 구매하는 과정이 시작됩니다. 이때 구매 과정에서 페이지가 자동으로 스크롤되며, 현재 몇 페이지의 에셋을 구매 중인지 확인할 수 있습니다. 구매가 완료되면 좌측에 구매 목록이 나타납니다.
4. 에러 해결 방법
- 즉시 에러 발생 시: 콘솔에 allow pasting 명령어를 입력한 후 스크립트를 다시 실행합니다.
- 중간에 멈출 경우: 현재 페이지에서 멈춘 번호를 확인하고, 스크립트의 start page 부분에 해당 페이지에서 1을 뺀 값을 입력해 다시 실행합니다.
5. 구매 확인
구매가 끝나면 메인 화면의 에셋 수와 실제 구매한 에셋 수가 일치하는지 확인합니다. 만약 숫자가 맞지 않는다면 스크립트를 다시 실행해 누락된 에셋을 구매하면 됩니다.
결론
2025년 이후 퀵셀 메가스캔 유료화를 대비하여, 필요한 에셋을 미리 확보하는 것이 중요합니다. 위의 방법을 통해 빠르게 대량의 에셋을 확보하고, 발생할 수 있는 에러에 유연하게 대처할 수 있습니다.
퀵셀 유료화를 대비하는 모든 사용자들에게 이 정보가 도움이 되길 바랍니다.
유료화 전에 무료 에셋을 최대한 확보하자!
Quixel Megascans의 유료화는 3D 아티스트와 개발자들에게 큰 변화입니다. 하지만 아직 기회는 남아 있습니다! 크롬 개발자 도구를 이용한 스크립트 덕분에 여러분은 무료 에셋들을 더 빠르고 효율적으로 다운로드할 수 있습니다.
유료화 전까지 시간이 많지 않으니, 서둘러 필요한 에셋을 다운로드해 두세요. 여러분의 작업 시간을 절약해줄 이 스크립트가 프로젝트에 큰 도움이 되길 바랍니다!
관련 링크
https://quixel.com/megascans/collections
https://gist.github.com/jamiephan/0c04986c7f2e62d5c87c4e8c8ce115fc#file-run-js
스크립트로 받은 에셋들을 활용해 프로젝트를 어떻게 완성했는지 댓글로 공유해 주세요! 😊
'Unreal > News' 카테고리의 다른 글
Unreal Engine Marketplace, 통합 플랫폼 Fab으로의 변화 (2) | 2024.11.10 |
---|---|
Unreal News GDC(Game Developer Conference) 2024 (0) | 2024.02.16 |
Unreal News 2024년 2월 언리얼 마켓플레이스 무료 콘텐츠 (0) | 2024.02.16 |
Unreal News 언리얼 페스트 2024 (0) | 2024.02.16 |