Download from ambientCG
Download from ambientCG

Download from ambientCG

What is this?

I discovered ambientCG.com by watching Gvarados' video about custom materials. They have over a thousand assets that are completely free to use. Getting them manually is a lot of work. Therefor I made this script that gets all of them, and adds a VMAT file for them.

Note: these are custom textures so the scaling might not be perfect. When generating the .vmat I set the scaling to 3.000. This can be changed in the code (mentioned below).

How does it work?

  • Installs pip module:
    • Requests
    • Pillow
  • Prompt for the location where you want to place the assets
    • I recommend using C:\Program Files (x86)\Steam\steamapps\common\sbox\addons
  • Prompt for the directory name (this name will be in the asset browser filters)
    • Eg. custom_materials
  • Create a tmp directory in the directory you provided, and put the downloads in there (this will be removed later)
  • Download the assets, and put them in the tmp directory
  • Change the filenames to lowercase (for the directories)
  • Unpack the zip files in the parent directory
  • Change the filenames to lowercase
  • Check if the file dimensions are correct (eg. 1024x1024 = ok, 1024x512 = ok. 1024x300 = not ok)
  • Create a VMAT file in every directory, and include if available:
    • Color map
    • Normal map
    • Metal map
    • Roughness map
    • Ambient Occlusion map
  • Create resolution txt files
  • Remove the downloaded zip files in the tmp directory
  • Remove the unused displacement images
⚠️

After the script is done, open your asset browser, and filter on the newly added mod. You will notice a lot of yellow icons with a grey ball in it. Also your asset browser will hang. This is because it's now generating a .vmat_c and .vtex_c files. Every time you scroll and you see those icons it will generate those files. So this might take a while. After a few seconds/minutes the materials will show up.

image
image

Using the Script

Prerequisites

You need to have Python 3 installed.

Code

In the code there are some variables you can change to customize the download and generated files.

###############################################################################
# Vars
###############################################################################

# Download options
downloadType = 'PhotoTexturePBR' # Options: PhotoTexturePBR, DecalPBR, AtlasPBR, PhotoTexturePlain, SBSAR, 3DModel, Terrain
downloadSort = 'Alphabet' # Options: Latest, Popular, Alphabet, Downloads
downloadResolution = '1K' # Options: 1K, 2K, 4k, 8K, 16K
downloadFormat = 'JPG' # Options: JPG, PNG (stats same texture: JPG = 262 KB vs. PNG = 1.833 KB)

downloadSbsarQuality = 'HQ' # Options: LQ, HQ

# Material settings
shader = 'simple.vfx'
textureCoordScaleX = '3.000'
textureCoordScaleY = '3.000'

# Resolution options: 
resolutionColor = '1024'
resolutionNormal = '1024'
resolutionRoughness = '1024'
resolutionMetallness = '1024'
resolutionAmbientOcclusion = '1024'
ambientCG.py15.0KB

Raw code
###############################################################################
# Created by: PoisonMichael
# Last update: 2021-08-09
# Website: poisonmichael.com
###############################################################################

###############################################################################
# Install module: requests and pillow
###############################################################################

import os
os.system("pip install requests --quiet --disable-pip-version-check")
os.system("pip install pillow --quiet --disable-pip-version-check")

###############################################################################
# Imports
###############################################################################

import requests
import zipfile
import shutil
import glob
from PIL import Image

###############################################################################
# Vars
###############################################################################

# Download options
downloadType = 'PhotoTexturePBR' # Options: PhotoTexturePBR, DecalPBR, AtlasPBR, PhotoTexturePlain, SBSAR, 3DModel, Terrain
downloadSort = 'Alphabet' # Options: Latest, Popular, Alphabet, Downloads
downloadResolution = '1K' # Options: 1K, 2K, 4k, 8K, 16K
downloadFormat = 'JPG' # Options: JPG, PNG (stats same texture: JPG = 262 KB vs. PNG = 1.833 KB)

downloadSbsarQuality = 'HQ' # Options: LQ, HQ

# Material settings
shader = 'simple.vfx'
textureCoordScaleX = '3.000'
textureCoordScaleY = '3.000'

# Resolution options: 
resolutionColor = '1024'
resolutionNormal = '1024'
resolutionRoughness = '1024'
resolutionMetallness = '1024'
resolutionAmbientOcclusion = '1024'

###############################################################################
# Ask user for input
###############################################################################

print('Enter the directory where you want to place the materials (typically in C:\Program Files (x86)\Steam\steamapps\common\sbox\addons):')
dest = input()

print('\nEnter the directory name (eg. custom_materials):')
dirName = input()

print()

root = dest + '\\' + dirName

###############################################################################
# Get the list of assets
###############################################################################

# Human readable file size
def sizeFormat(num, suffix='B'):
    for unit in [' ',' K',' M',' G',' T',' P',' E',' Z']:
        if abs(num) < 1024.0:
            return "%3.1f%s%s" % (num, unit, suffix)
        num /= 1024.0
    return "%.1f%s%s" % (num, 'Y', suffix)

# Check if the material location already exists, and create it if it's doesn't
if not os.path.exists(root):
  os.mkdir(root)

# Check if the tmp location already exists, and create it if it's doesn't. This is used to temporarily store the assets
if not os.path.exists(root + '\\tmp'):
  os.mkdir(root + '\\tmp')

###############################################################################
# Get the total amount of assets
###############################################################################

url = "https://ambientCG.com/api/v2/full_json?limit=1&method&type=PhotoTexturePBR&sort=Latest"
headers = {
  'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.131 Safari/537.36'
}
response = requests.request("GET", url, headers=headers).json()

# Counters
assetCounter = 0
assetOffset = 0
assetTotal = int(response['numberOfResults'])
assetTotalCounter = 0
assetTotalSize = 0

###############################################################################
# Get all of the assets
###############################################################################

while assetCounter <= (assetOffset + 100) and assetCounter < assetTotal:
  if assetCounter == (assetOffset + 100):
    assetOffset += 100

  # API url of ambiantCG
  url = "https://ambientCG.com/api/v2/full_json?limit=100&type=" + str(downloadType) + "&sort=" + str(downloadSort) + "&offset=" + str(assetOffset) + "&include=downloadData&date"

  # We need this header to prevent 403 error
  headers = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.131 Safari/537.36'
  }

  # Do a GET request
  response = requests.request("GET", url, headers=headers).json()

  ###############################################################################
  # Loop over the results to get the info we need
  ###############################################################################

  for asset in response['foundAssets']:
    assetId = asset['assetId'].lower() # Make the name lowercase
    assetDir = root + '\\' + assetId + '_' + downloadResolution.lower()
    assetIdDir = assetDir + '\\' + assetId + '_' + downloadResolution.lower()

    # Zip files
    if 'zip' in asset['downloadFolders']['/']['downloadFiletypeCategories']:
      # Loop over the available zip files
      for assetDownload in asset['downloadFolders']['/']['downloadFiletypeCategories']['zip']['downloads']:
        # Get the assets that match your resolution and format
        if assetDownload['attribute'] == downloadResolution.upper() + '-' + downloadFormat.upper():

          ###############################################################################
          # Download zip file
          ###############################################################################

          download = assetDownload['downloadLink']

          downloadZip = requests.get(download, headers=headers, allow_redirects=True)

          zip = open(root + '\\tmp\\' + assetId + '.zip', 'wb')
          zip.write(downloadZip.content)
          zip.close()

          # Extract zip file
          with zipfile.ZipFile(root + '\\tmp\\' + assetId + '.zip', 'r') as zip_ref:
            zip_ref.extractall(assetDir)

          # Make all the files lowercase
          files = os.listdir(assetDir)
          for file in files:
            os.rename(assetDir + '\\' + file, assetDir + '\\' + file.lower())

          # Check image dimensions (need to be a square, or half size like 1024x512, 1000x300 = not ok)
          image = Image.open(assetDir + '\\' + file)
          width, height = image.size
          image.close()

          if (width == height) or (width - height == 0) or (width - height == height) or (height - width == width):
            # Counter and file size
            assetTotalCounter += 1
            print("{}) {}".format(assetTotalCounter, assetId))
            assetTotalSize = assetTotalSize + int(assetDownload['size'])

            ###############################################################################
            # Create VMAT file
            ###############################################################################

            vmat = open(assetIdDir + '.vmat',"w+")

            # Check for color map
            if os.path.exists(assetIdDir + '_color.' + downloadFormat.lower()):
              color = "  //---- Color ----\n  g_flModelTintAmount \"1.000\"\n  g_vColorTint \"[1.000000 1.000000 1.000000 0.000000]\"\n  TextureColor \"" + assetId + "_" + downloadResolution.lower() + "/" + assetId + "_" + downloadResolution.lower() + "_color." + downloadFormat.lower() + "\"\n\n"
            else:
              color = "  //---- Color ----\n  g_flModelTintAmount \"1.000\"\n  g_vColorTint \"[1.000000 1.000000 1.000000 0.000000]\"\n  TextureColor \"materials/default/default_color.tga\"\n\n"
            
            # Check for normal map
            if os.path.exists(assetIdDir + '_normal.' + downloadFormat.lower()):
              normal = "  //---- Normal ----\n  TextureColor \"" + assetId + "_" + downloadResolution.lower() + "/" + assetId + "_" + downloadResolution.lower() + "_normal." + downloadFormat.lower() + "\"\n\n"
            else:
              normal = "  //---- Normal ----\n  TextureColor \"materials/default/default_normal.tga\"\n\n"

            # Check for roughness map
            if os.path.exists(assetIdDir + '_roughness.' + downloadFormat.lower()):
              roughness = "  //---- Roughness ----\n  TextureColor \"" + assetId + "_" + downloadResolution.lower() + "/" + assetId + "_" + downloadResolution.lower() + "_roughness." + downloadFormat.lower() + "\"\n\n"
            else:
              roughness = "  //---- Roughness ----\n  TextureColor \"materials/default/default_rough.tga\"\n\n"

            # Check for metal map
            if os.path.exists(assetIdDir + '_metalness.' + downloadFormat.lower()):
              metalness = "  //---- PBR ----\n  F_METALNESS_TEXTURE 1\n\n  //---- Metalness ----\n  TextureMetalness \"" + assetId + "_" + downloadResolution.lower() + "/" + assetId + "_" + downloadResolution.lower() + "_metalness." + downloadFormat.lower() + "\"\n\n"
            else:
              metalness = '//---- Metalness ----\n  g_flMetalness \"0.000\"\n\n'

            # Check for ambient occlusion
            if os.path.exists(assetIdDir + '_ambientocclusion.' + downloadFormat.lower()):
              ambientOcclusion = "  //---- PBR ----\n  F_AMBIENT_OCCLUSION_TEXTURE 1\n\n  //---- Ambient Occlusion ----\n  TextureAmbientOcclusion \"" + assetId + "_" + downloadResolution.lower() + "/" + assetId + "_" + downloadResolution.lower() + "_ambientocclusion." + downloadFormat.lower() + "\"\n\n"
            else:
              ambientOcclusion = ''

            # Template to put in the VMAT file
            templateVMAT = [
              "// THIS FILE IS AUTO-GENERATED\n",
              "\n",
              "Layer0\n",
              "{\n",
              "  shader \"" + shader + "\"\n",
              "\n",
              color,
              normal,
              roughness,
              metalness,
              ambientOcclusion,
              "  //---- Fade ----\n",
              "  g_flFadeExponent \"1.000\"\n",
              "\n",
              "  //---- Fog ----\n",
              "  g_bFogEnabled \"1\"\n",
              "\n",
              "  //---- Lighting ----\n",
              "  g_flDirectionalLightmapMinZ \"0.050\"\n",
              "  g_flDirectionalLightmapStrength \"1.000\"\n",
              "\n",
              "  //---- Texture Coordinates ----\n",
              "  g_nScaleTexCoordUByModelScaleAxis \"0\"\n",
              "  g_nScaleTexCoordVByModelScaleAxis \"0\"\n",
              "  g_vTexCoordOffset \"[0.000 0.000]\"\n",
              "  g_vTexCoordScale \"[" + textureCoordScaleX + " " + textureCoordScaleY + "]\"\n",
              "  g_vTexCoordScrollSpeed \"[0.000 0.000]\"\n",
              "}"
            ]
            
            vmat.writelines(templateVMAT)
            vmat.close()

            ###############################################################################
            # Create resolution txt files
            ###############################################################################

            templateResolutionColor = [
              "settings\n",
              "{\n",
              "  \"maxres\" \"" + str(resolutionColor) + "\"\n",
              "}"
            ]

            templateResolutionNormal = [
              "settings\n",
              "{\n",
              "  \"maxres\" \"" + str(resolutionNormal) + "\"\n",
              "}"
            ]

            templateResolutionRoughness = [
              "settings\n",
              "{\n",
              "  \"maxres\" \"" + str(resolutionRoughness) + "\"\n",
              "}"
            ]

            templateResolutionMetalness = [
              "settings\n",
              "{\n",
              "  \"maxres\" \"" + str(resolutionMetallness) + "\"\n",
              "}"
            ]

            templateResolutionAmbientOcclusion = [
              "settings\n",
              "{\n",
              "  \"maxres\" \"" + str(resolutionAmbientOcclusion) + "\"\n",
              "}"
            ]

            # Create resolution txt files if needed
            if os.path.exists(assetIdDir + '_color.' + downloadFormat.lower() + ''):
              makeColorTxt = open(assetIdDir + '_color.txt',"w+")
              makeColorTxt.writelines(templateResolutionColor)
              makeColorTxt.close()
            
            if os.path.exists(assetIdDir + '_normal.' + downloadFormat.lower() + ''):
              makeNormalTxt = open(assetIdDir + '_normal.txt',"w+")
              makeNormalTxt.writelines(templateResolutionNormal)
              makeNormalTxt.close()
            
            if os.path.exists(assetIdDir + '_roughness.' + downloadFormat.lower() + ''):
              makeRoughnessTxt = open(assetIdDir + '_roughness.txt',"w+")
              makeRoughnessTxt.writelines(templateResolutionRoughness)
              makeRoughnessTxt.close()
            
            if os.path.exists(assetIdDir + '_metalness.' + downloadFormat.lower() + ''):
              makeMetalnessTxt = open(assetIdDir + '_metalness.txt',"w+")
              makeMetalnessTxt.writelines(templateResolutionMetalness)
              makeMetalnessTxt.close()
            
            if os.path.exists(assetIdDir + '_ambientocclusion.' + downloadFormat.lower() + ''):
              makeAmbientOcclusionTxt = open(assetIdDir + '_ambientocclusion.txt',"w+")
              makeAmbientOcclusionTxt.writelines(templateResolutionAmbientOcclusion)
              makeAmbientOcclusionTxt.close()
          else:
            # Remove asset directory
            shutil.rmtree(assetDir)

    elif 'sbsar' in asset['downloadFolders']['/']['downloadFiletypeCategories']:
      # Loop over the available sbsar files
      for assetDownload in asset['downloadFolders']['/']['downloadFiletypeCategories']['sbsar']['downloads']:
        # Get the assets that match your quality
        if (assetDownload['attribute'] == downloadSbsarQuality.upper()) or (assetDownload['attribute'] == ''):

          ###############################################################################
          # Download sbsar file
          ###############################################################################

          # Counter and file size
          assetTotalCounter += 1
          print("{}) {}".format(assetTotalCounter, assetId))
          assetTotalSize = assetTotalSize + int(assetDownload['size'])
          
          download = assetDownload['downloadLink']

          downloadSbsar = requests.get(download, headers=headers, allow_redirects=True)

          sbsar = open(root + '\\' + assetId + '.sbsar', 'wb')
          sbsar.write(downloadSbsar.content)
          sbsar.close()

  assetCounter += 100

# Remove temporary directory with all the zip filesos.remove(filePath)
shutil.rmtree(root + '\\tmp')

#  Remove the unused *displacement.jpg files
fileList = glob.glob(root + '\\*\\*displacement.jpg')
for filePath in fileList:
  os.remove(filePath)

# Print
print("\nFinished!\n")
print('Total downloaded assets: ' + str(assetTotalCounter))
print('Total download size: ' + str(sizeFormat(assetTotalSize)) + "\n")
print('Assets stored in: ' + root + "\n")
os.system("pause")
Output example
Enter the directory where you want to place the materials (typically in C:\Program Files (x86)\Steam\steamapps\common\sboxddons):
C:\Program Files (x86)\Steam\steamapps\common\sbox\addons

Enter the directory name (eg. custom_materials):
custom_materials

1) acousticfoam001
...
1086) woodsiding009

Finished!

Total downloaded assets: 1083
Total download size: 4.0 GB

Assets stored in: C:\Program Files (x86)\Steam\steamapps\common\sbox\addons\custom_materials

Stats

As of Aug. 9, 2021 this script will download 1075 assets. And is 4 GB in size. Running the script takes around 15 min.

Direct Download

If you don't want to download the latest assets with the Python script you can download this zip from my Google Drive.

As of Aug. 9th, 2021

  • Assets: 1075
  • Resolution: 1024
  • Total size: 4 GB
  • Includes:
    • .vmat
    • .vmat_c
    • .vtex_c
    • .txt

Extract in C:\Program Files (x86)\Steam\steamapps\common\sbox\addons
Contains assets from ambientCG.com, licensed under CC0 1.0 Universal.