Skip to content

Instantly share code, notes, and snippets.

@scurest
Last active September 7, 2017 23:23
Show Gist options
  • Save scurest/3c67b60e900f4142436730eef8b68f92 to your computer and use it in GitHub Desktop.
Save scurest/3c67b60e900f4142436730eef8b68f92 to your computer and use it in GitHub Desktop.
Generates MetalRoughSpheres
#!/bin/env python
"""Generates MetalRoughSpheres.
Output is written to MetalRoughSpheres.gltf and MetalRoughSpheres.bin.
"""
from math import sqrt
import base64
import json
import struct
# http://blog.andreaskahler.com/2009/06/creating-icosphere-mesh-in-code.html
class Icosphere:
def vert(self, co, uv):
mag = sqrt(sum([x**2 for x in co]))
normalized = [x/mag for x in co]
self.positions.append(normalized)
self.uvs.append(uv)
def __init__(self):
"""Build a regular icosahedron."""
self.positions = []
self.uvs = []
self.tris = []
self.vert([-0.525731, 0.850651, 0.0], [0.3397, 0.694])
self.vert([-0.850651, -0.0, 0.525731], [0.3397, 0.5089])
self.vert([0.0, 0.525731, 0.850651], [0.5, 0.6015])
self.vert([0.525731, 0.850651, 0.0], [0.5, 0.7866])
self.vert([0.525731, 0.850651, 0.0], [0.3397, 0.8791])
self.vert([0.0, 0.525731, -0.850651], [0.1794, 0.7866])
self.vert([-0.850651, 0.0, -0.525731], [0.1794, 0.6015])
self.vert([0.525731, 0.850651, 0.0], [0.6603, 0.694])
self.vert([0.850651, -0.0, 0.525731], [0.6603, 0.5089])
self.vert([0.0, -0.525731, 0.850651], [0.5, 0.4164])
self.vert([-0.525731, -0.850651, -0.0], [0.1794, 0.4164])
self.vert([0.0, -0.525731, -0.850651], [0.0191, 0.694])
self.vert([0.0, 0.525731, -0.850651], [0.9809, 0.3238])
self.vert([0.525731, 0.850651, 0.0], [0.9809, 0.5089])
self.vert([0.850651, 0.0, -0.525731], [0.8206, 0.4164])
self.vert([0.525731, -0.850651, -0.0], [0.6603, 0.3238])
self.vert([-0.525731, -0.850651, -0.0], [0.5, 0.2313])
self.vert([-0.525731, -0.850651, -0.0], [0.6603, 0.1388])
self.vert([0.0, -0.525731, -0.850651], [0.8206, 0.2313])
self.vert([-0.525731, -0.850651, -0.0], [0.3397, 0.3238])
self.vert([-0.525731, -0.850651, -0.0], [0.0191, 0.5089])
self.vert([0.525731, 0.850651, 0.0], [0.8206, 0.6015])
self.tris.append([0, 1, 2])
self.tris.append([0, 2, 3])
self.tris.append([0, 4, 5])
self.tris.append([0, 5, 6])
self.tris.append([0, 6, 1])
self.tris.append([7, 2, 8])
self.tris.append([2, 1, 9])
self.tris.append([1, 6, 10])
self.tris.append([6, 5, 11])
self.tris.append([12, 13, 14])
self.tris.append([15, 8, 9])
self.tris.append([15, 9, 16])
self.tris.append([15, 17, 18])
self.tris.append([15, 18, 14])
self.tris.append([15, 14, 8])
self.tris.append([9, 8, 2])
self.tris.append([19, 9, 1])
self.tris.append([11, 20, 6])
self.tris.append([14, 18, 12])
self.tris.append([8, 14, 21])
def refine(self):
midpoint_cache = {}
def midpoint(v1, v2):
v1, v2 = sorted((v1, v2))
if (v1, v2) in midpoint_cache:
return midpoint_cache[(v1, v2)]
midpos = [(x+y)/2 for x,y in zip(self.positions[v1], self.positions[v2])]
miduv = [(x+y)/2 for x,y in zip(self.uvs[v1], self.uvs[v2])]
self.vert(midpos, miduv)
index = len(self.positions) - 1
midpoint_cache[(v1, v2)] = index
return index
new_tris = []
for tri in self.tris:
# /\ /\
# / \ ==> /__\
# / \ ==> /\ /\
# /_____ \ /__\/__\
a = midpoint(tri[0], tri[1])
b = midpoint(tri[1], tri[2])
c = midpoint(tri[2], tri[0])
new_tris.append([tri[0], a, c])
new_tris.append([tri[1], b, a])
new_tris.append([tri[2], c, b])
new_tris.append([a, b, c])
self.tris = new_tris
class MetalRoughSpheres:
def add_buffer_data(self, data, align=1):
off = len(self.buf)
if off % align != 0:
self.buf += 'b\0' * ((- off) % align)
self.buf += data
return off
def add_buffer_view(self, data, align=1, stride=None):
data_off = self.add_buffer_data(data, align=align)
view = {
'buffer': 0,
'byteOffset': data_off,
'byteLength': len(data),
}
if stride:
view['byteStride'] = stride
self.gltf['bufferViews'].append(view)
buffer_view_idx = len(self.gltf['bufferViews']) - 1
return buffer_view_idx
def add_accessor(self, accessor):
self.gltf['accessors'].append(accessor)
return len(self.gltf['accessors']) - 1
def finish(self, gltf_path, bin_path):
"""Write the output."""
self.gltf['buffers'] = [{
'uri': bin_path,
'byteLength': len(self.buf),
}]
with open(gltf_path, 'w+') as fp:
json.dump(self.gltf, fp, indent=4)
with open(bin_path, 'wb+') as fp:
fp.write(self.buf)
def create_spheres(self):
# Build an icosphere
icosphere = Icosphere()
for _ in range(0, 4):
icosphere.refine()
pos = [x for v in icosphere.positions for x in v]
uvs = [x for v in icosphere.uvs for x in v]
indices = [i for tri in icosphere.tris for i in tri]
assert(len(indices) < 2**16) # Make sure we can use u16 for the index data
# Write buffers/bufferViews/accessors for the icosphere positions/indices.
# These are reused for every sphere.
pos_data = struct.pack('<%sf' % len(pos), *pos)
index_data = struct.pack('<%sH' % len(indices), *indices)
pos_buffer_view =self.add_buffer_view(pos_data, align=4)
index_buffer_view =self.add_buffer_view(index_data, align=2)
pos_accessor = self.add_accessor({
'bufferView': pos_buffer_view,
'type': 'VEC3',
'componentType': 5126, # float
'count': len(pos)//3,
'min': [-1, -1, -1],
'max': [1, 1, 1],
})
index_accessor = self.add_accessor({
'bufferView': index_buffer_view,
'type': 'SCALAR',
'componentType': 5123, # ushort
'count': len(indices),
})
# Write data for the texcoords for every sphere.
# We'll pack them all into one bufferview.
uv_data_list = []
# Scale down and move the UVs away from the gutters where the labels are.
# |gutter|col1| col2 | col3 |...
# The UVs should fit into col1.
col_width = 1/14
gutter_width = 24/1024
scale = col_width - gutter_width
for off in range(0, len(uvs), 2):
uvs[off] *= scale
uvs[off] += gutter_width
uvs[off+1] *= scale
for du in [0, col_width]:
for i in range(0, 7):
for j in range(0, 7):
new_uvs = uvs[:]
# Translate into position
for off in range(0, len(new_uvs), 2):
new_uvs[off] += i * 2 * col_width + du
new_uvs[off+1] += j * 2 * col_width
uv_data_list.append(struct.pack('<%sf' % len(new_uvs), *new_uvs))
uv_data = b''.join(uv_data_list)
uv_buffer_view = self.add_buffer_view(uv_data, align=4, stride=8)
uv_byte_length = len(uvs) * 4
# Create the grid of spheres; one mesh/texcoord accessor for every sphere.
num_spheres = 7 * 7 * 2
self.gltf['scene'] = 0
self.gltf['scenes'] = [ { 'nodes': [0] } ]
self.gltf['nodes'] = [{
'scale': [0.4, 0.4, 0.4],
'children': list(range(1, 1+num_spheres)),
}]
idx = 0
for z in [0, 8]:
for i in range(0, 7):
for j in range(0, 7):
uv_accessor = self.add_accessor({
'bufferView': uv_buffer_view,
'type': 'VEC2',
'componentType': 5126, # float
'count': len(uvs)//2,
'byteOffset': idx * uv_byte_length,
})
self.gltf['meshes'].append({
'primitives': [{
'attributes': {
'POSITION': pos_accessor,
'NORMAL': pos_accessor,
'TEXCOORD_0': uv_accessor
},
'indices': index_accessor,
'material': 0,
}]
})
mesh_id = len(self.gltf['meshes']) - 1
x = -3*i + 9
y = -3*j + 9
self.gltf['nodes'].append({
'name': 'M(%d, %d)' % (i,j), # useful for debugging
'mesh': mesh_id,
'translation': [x, y, z],
})
idx += 1
def create_labels(self):
# Create the metal/non-metal and rough/smooth labels.
# This data was dumped out of MetalRoughSpheres.blend.
pos = [
4.607307, -12.359449, 0,
10.713691, -11.081038, 0,
10.713691, -12.359448, 0,
-11.874729, -12.359450, 0,
-3.906647, -11.081039, 0,
-3.906647, -12.359449, 0,
10.982860, 3.929994, 0,
12.186588, 10.969941, 0,
12.186589, 3.929994, 0,
10.713691, -12.359448, 8,
4.607307, -11.081039, 8,
4.607307, -12.359449, 8,
12.186590, -11.753379, 0,
10.982860, -3.775491, 0,
12.186589, -3.775488, 0,
-3.906648, -12.359449, 8,
-11.874729, -11.081040, 8,
-11.874729, -12.359450, 8,
12.186589, 3.929994, 8,
10.982859, 10.969941, 8,
10.982860, 3.929994, 8,
10.982861, -11.753379, 8,
12.186589, -3.775491, 8,
10.982860, -3.775488, 8,
4.607307, -11.081039, 0,
-11.874729, -11.081040, 0,
10.982859, 10.969941, 0,
10.713691, -11.081038, 8,
10.982861, -11.753379, 0,
-3.906648, -11.081039, 8,
12.186588, 10.969941, 8,
12.186590, -11.753379, 8,
]
normals = [
0, 0, -1,
0, 0, -1,
0, 0, -1,
0, 0, -1,
0, 0, -1,
0, 0, -1,
0, 0, -1,
0, 0, -1,
0, 0, -1,
0, 0, 1,
0, 0, 1,
0, 0, 1,
0, 0, -1,
0, 0, -1,
0, 0, -1,
0, 0, 1,
0, 0, 1,
0, 0, 1,
0, 0, 1,
0, 0, 1,
0, 0, 1,
0, 0, 1,
0, 0, 1,
0, 0, 1,
0, 0, -1,
0, 0, -1,
0, 0, -1,
0, 0, 1,
0, 0, -1,
0, 0, 1,
0, 0, 1,
0, 0, 1,
]
uvs = [
0.1175, 0.9988,
0.0205, 0.9785,
0.0205, 0.9988,
0.9870, 0.9988,
0.8604, 0.9785,
0.8604, 0.9988,
0.0205, 0.1284,
0.0014, 0.0166,
0.0014, 0.1284,
0.1175, 0.9988,
0.0205, 0.9785,
0.0205, 0.9988,
0.0014, 0.9988,
0.0205, 0.8721,
0.0014, 0.8721,
0.9870, 0.9988,
0.8604, 0.9785,
0.8604, 0.9988,
0.0205, 0.1284,
0.0014, 0.0166,
0.0014, 0.1284,
0.0014, 0.9988,
0.0205, 0.8721,
0.0014, 0.8721,
0.1175, 0.9785,
0.9870, 0.9785,
0.0205, 0.0166,
0.1175, 0.9785,
0.0205, 0.9988,
0.9870, 0.9785,
0.0205, 0.0166,
0.0205, 0.9988,
]
indices = [
0, 1, 2,
3, 4, 5,
6, 7, 8,
9, 10, 11,
12, 13, 14,
15, 16, 17,
18, 19, 20,
21, 22, 23,
0, 24, 1,
3, 25, 4,
6, 26, 7,
9, 27, 10,
12, 28, 13,
15, 29, 16,
18, 30, 19,
21, 31, 22,
]
assert(len(indices) < 2**8)
pos_data = struct.pack('<%sf' % len(pos), *pos)
normal_data = struct.pack('<%sf' % len(normals), *normals)
uv_data = struct.pack('<%sf' % len(uvs), *uvs)
index_data = struct.pack('<%sB' % len(indices), *indices)
pos_buffer_view =self.add_buffer_view(pos_data, align=4)
normal_buffer_view =self.add_buffer_view(normal_data, align=4)
uv_buffer_view =self.add_buffer_view(uv_data, align=4)
index_buffer_view =self.add_buffer_view(index_data, align=1)
pos_accessor = self.add_accessor({
'bufferView': pos_buffer_view,
'type': 'VEC3',
'componentType': 5126, # float
'count': len(pos)//3,
'min': [-11.874729, -12.35945, 0],
'max': [12.186589, 10.969941, 8],
})
normal_accessor = self.add_accessor({
'bufferView': normal_buffer_view,
'type': 'VEC3',
'componentType': 5126, # float
'count': len(normals)//3,
})
uv_accessor = self.add_accessor({
'bufferView': uv_buffer_view,
'type': 'VEC2',
'componentType': 5126, # float
'count': len(uvs)//2,
})
index_accessor = self.add_accessor({
'bufferView': index_buffer_view,
'type': 'SCALAR',
'componentType': 5121, # ubyte
'count': len(indices),
})
self.gltf['meshes'].append({
'name': 'Labels',
'primitives': [{
'attributes': {
'POSITION': pos_accessor,
'NORMAL': normal_accessor,
'TEXCOORD_0': uv_accessor,
},
'indices': index_accessor,
'material': 0,
}]
})
mesh_id = len(self.gltf['meshes']) - 1
self.gltf['nodes'].append({
'name': 'Labels',
'mesh': mesh_id,
})
node_id = len(self.gltf['nodes']) - 1
self.gltf['nodes'][0]['children'].append(node_id)
def __init__(self):
self.buf = b''
self.gltf = {
'asset': { 'version' : '2.0' },
'nodes': [],
'meshes': [],
'accessors': [],
'materials': [
{
'pbrMetallicRoughness': {
'baseColorTexture': { 'index': 0 },
'metallicRoughnessTexture': { 'index': 1 },
}
}
],
'textures': [
{ 'sampler': 0, 'source': 0 },
{ 'sampler': 0, 'source': 1 },
],
'images': [
{ 'uri': 'Spheres_BaseColor.png' },
{ 'uri': 'Spheres_MetalRough.png' },
],
'samplers': [
{
'magFilter': 9729,
'minFilter': 9986,
'wrapS': 33071,
'wrapT': 33071
}
],
'bufferViews': [],
'buffers': [],
}
self.create_spheres()
self.create_labels()
mrs = MetalRoughSpheres()
mrs.finish('MetalRoughSpheres.gltf', 'MetalRoughSpheres.bin')
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment