Megascans 란???
3D 모델을 생성할때 Blender나 Maya등을 사용해서 나무깎는 장인처럼 만드는 방식은 개인 개발자나 회사 개발자나 시간이 너무 오래 걸린다.
이를 극복하고자 여러 3D모델 라이브러리를 만들어 다운로드하여 사용하는데, Megascans 또한 이러한 3D 라이브러리들중 초고해상도 모델 라이브러리이다.
Megascans는 Quixel이 제공하는 고해상도 3D 자산 라이브러리로, 실제 물체를 스캔하여 생성된 텍스처와 모델을 포함하고 있는데, 이 라이브러리는 자연 환경, 건축물, 소품 등 다양한 카테고리의 자산을 제공하여, 사용자들이 사실적인 비주얼을 쉽게 구현할 수 있게하는 "무료" 라이브러리이다.
근데 왜 유료로 바뀜?
Quixel은 Megascans를 Epic Games의 새로운 통합 마켓플레이스인 Fab으로 이전하게 되면서, 기존의 무료 라이브러리들을 유료로 (구독 서비스)로 변경된다고 공지하였는데, 이를 통해 지속적인 관리 및 새로운 모델 생성 비용을 충당한다고 한다.
전환 시기: Megascans는 2024년 10월에 Fab 마켓플레이스가 출시되면서 유료 서비스로 전환될 예정이다.
미리 등록한 모델은??? : 유료 전환 이후에도 기존에 구매한 자산은 Bridge를 통해 계속 접근할 수 있다, 하지만 새로운 모델들은 Fab에서만 제공될 것이다.
일일히 1개씩 등록해야 하나요? NO!!
18876개의 Megascans 모델들이 있는데 일일히 1개씩 클릭만해도 한세월이 걸릴 것이다,
다행히 어떤 능력자가 크롬 브라우저에서 동작하는(Edge에서도 가능하다.) javascript를 작성하였다.
사용방법은 아래와 같다.
1. https://quixel.com/megascans/collections 로 이동한다.
2. 로그인 or 회원가입 한다.(SNS / Epic Games 로그인을 하면 된다.)
3. F12를 눌러 개발자 도구를 활성화하고, Console탭으로 이동한다.
4. 콘솔창에 allow pasting를 입력 후 Enter!
5. 아래 Javascript를 복붙한다음 Enter!!
(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.");
}
})();
위 코드는 아래 Github에서 제공하는 코드입니다, 하트 한번씩 눌러주세요 ^0^
A script to automatically add ALL items to your account in quixel
A script to automatically add ALL items to your account in quixel - README.md
gist.github.com
6. 그럼 뭔가가 자동으로 실행될텐데 잠시 기다려준다(약 10분정도 소요된다.)
7. 만약 중간에 에러가 발생한다면,
에러가 발생한 페이지 -1 (ex 3페이지라면 2)를 위 코드에 반영후 다시 돌리면 된다.
(async (startPage = 2, autoClearConsole = true) => {
8. 좌측 메뉴의 Purchased에 18876이 있으면 완료된것이다!!
코드 실행에 관해 궁금한 점이 있으면 댓글로 문의바랍니다 ^^
끝.
'자투리 정보' 카테고리의 다른 글
[피싱/사기]위택스 사칭 이메일 피싱 조심하세요!! 주민세 체납 독촉장 / 사기꾼 메일 주소 공유 / 로그인 했을때 대처하는 법, 대처법 (0) | 2024.10.08 |
---|---|
[이스라엘/레바논/이란] 도대체 얘들을 왜 맨날 싸울까??? -2 제1차 중동 전쟁 발발 (0) | 2024.10.03 |
[카카오 방문 택배]용달 부르긴 애매한 이삿짐 박스 방문 택배 보내기 / 택배 픽업 서비스 (5) | 2024.09.24 |
[보이스피싱]경찰청 등기 사기 주의!! (3) | 2024.09.24 |
[Linux/리눅스]프로세스명으로 kill하는 명령어 (0) | 2024.09.23 |