본문 바로가기
자투리 정보

[Unreal / Unity / Blender] 초고해상도 3D 모델 라이브러리 이제 곧 유료되니 어서 등록해두세요(10월에 유료 전환), 하는 방법 포함(18876개)

by Mr.noobiest 2024. 9. 24.

 

Megascans 란???


3D 모델을 생성할때 Blender나 Maya등을 사용해서 나무깎는 장인처럼 만드는 방식은 개인 개발자나 회사 개발자나 시간이 너무 오래 걸린다.

이를 극복하고자 여러 3D모델 라이브러리를 만들어 다운로드하여 사용하는데, Megascans 또한 이러한 3D 라이브러리들중 초고해상도 모델 라이브러리이다.

 

Megascans는 Quixel이 제공하는 고해상도 3D 자산 라이브러리로, 실제 물체를 스캔하여 생성된 텍스처와 모델을 포함하고 있는데, 이 라이브러리는 자연 환경, 건축물, 소품 등 다양한 카테고리의 자산을 제공하여, 사용자들이 사실적인 비주얼을 쉽게 구현할 수 있게하는 "무료" 라이브러리이다.

 

Megascans로 빠른 개발이 가능하다.

 


 

근데 왜 유료로 바뀜?


Quixel은 Megascans를 Epic Games의 새로운 통합 마켓플레이스인 Fab으로 이전하게 되면서, 기존의 무료 라이브러리들을 유료로 (구독 서비스)로 변경된다고 공지하였는데, 이를 통해 지속적인 관리 및 새로운 모델 생성 비용을 충당한다고 한다.

전환 시기: Megascans는 2024년 10월에 Fab 마켓플레이스가 출시되면서 유료 서비스로 전환될 예정이다.

미리 등록한 모델은??? : 유료 전환 이후에도 기존에 구매한 자산은 Bridge를 통해 계속 접근할 수 있다, 하지만 새로운 모델들은 Fab에서만 제공될 것이다.

 

 


 

 

일일히 1개씩 등록해야 하나요? NO!!

 

18876개의 Megascans 모델들이 있는데 일일히 1개씩 클릭만해도 한세월이 걸릴 것이다,

행히 어떤 능력자가 크롬 브라우저에서 동작하는(Edge에서도 가능하다.) javascript를 작성하였다.

사용방법은 아래와 같다.

 


 

1. https://quixel.com/megascans/collections 로 이동한다.

 

URL이 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분정도 소요된다.)

 

총 19 페이지의 모든 모델들을 사용자 계정에 등록하는 중

 


 

 

 

7. 만약 중간에 에러가 발생한다면, 

 

에러가 발생한 페이지 -1 (ex 3페이지라면 2)를 위 코드에 반영후 다시 돌리면 된다.

 

(async (startPage = 2, autoClearConsole = true) => {

 

 


 

 

8. 좌측 메뉴의 Purchased에 18876이 있으면 완료된것이다!!

 

 

 

코드 실행에 관해 궁금한 점이 있으면 댓글로 문의바랍니다 ^^

 

끝.

 

 

728x90
반응형