Skip to content

Instantly share code, notes, and snippets.

@kice
Created April 30, 2020 22:53
Show Gist options
  • Save kice/216bee04b0480de46f6e8e3f25447967 to your computer and use it in GitHub Desktop.
Save kice/216bee04b0480de46f6e8e3f25447967 to your computer and use it in GitHub Desktop.
Convert MineCraft JSON model to Source engine model
import collections
import io
import json
import logging
import logging.handlers
import math
import os
import shutil
import struct
import subprocess
import sys
import argparse
import base64
from typing import List, Tuple
from PIL import Image
fmt = '[%(levelname)s] %(asctime)s:\t%(message)s'
formatter = logging.Formatter(fmt, datefmt="%Y-%m-%d %H:%M:%S")
stderr_handler = logging.StreamHandler(sys.stderr)
stderr_handler.setFormatter(formatter)
file_handler = logging.handlers.RotatingFileHandler(
'mcexport.log', backupCount=5, encoding='utf-8')
file_handler.setFormatter(formatter)
logger = logging.getLogger('mcexport')
logger.addHandler(stderr_handler)
logger.addHandler(file_handler)
logger.setLevel(logging.INFO)
def runcmd(cmd: str) -> Tuple[bool, int, str]:
try:
popen_params = {
"stdout": subprocess.PIPE,
"stderr": subprocess.PIPE,
"stdin": subprocess.DEVNULL,
}
if os.name == "nt":
popen_params["creationflags"] = 0x08000000
proc = subprocess.Popen(cmd, **popen_params)
stdout, stderr = proc.communicate()
return True, proc.returncode, stdout
except Exception as err:
return False, -1, err
reflect_lut = [(i / 255.0)**2.2 for i in range(256)]
def compute_reflectivity(data: bytes, channles=4) -> Tuple[float, float, float]:
if channles != 3 or channles != 4:
logger.error('unsupported texture format')
exit(1)
sX = sY = sZ = 0.0
for i in range(0, len(data), channles):
sX += reflect_lut[data[i]]
sY += reflect_lut[data[i + 1]]
sZ += reflect_lut[data[i + 2]]
inv = 1.0 / (len(data) / 4)
return sX * inv, sY * inv, sZ * inv
def save_vtf(images: List[Image.Image], fp: str, no_refle=False):
def i8(x): return struct.unpack('B', x.to_bytes(1, 'little'))
def i16(x): return struct.unpack('BB', x.to_bytes(2, 'little'))
def i32(x): return struct.unpack('BBBB', x.to_bytes(4, 'little'))
def f32(x): return struct.unpack('BBBB', struct.pack('f', float(x)))
if isinstance(images, Image.Image):
images = [images]
sX = sY = sZ = 0.0
width, height = images[0].size
mode = images[0].mode
if mode == 'RGB':
channels = 3
elif mode == 'RGBA':
channels = 4
else:
logger.error('unsupported texture format')
exit(1)
raws = []
for im in images:
if im.size == (width, height) or mode != im.mode:
logger.error('Animated texture mismatch')
exit(1)
raw = im.tobytes()
raws += [raw]
if not no_refle:
x, y, z = compute_reflectivity(raw, channels)
sX += x
sY += y
sZ += z
if no_refle:
sX = sY = sZ = 1.0
else:
inv = 1 / len(raws)
sX /= inv
sY /= inv
sZ /= inv
vtf = open(fp, 'wb')
# common vtf header, define v7.2
vtf.write(bytes([
0x56, 0x54, 0x46, 0x00, 0x07, 0x00, 0x00, 0x00,
0x02, 0x00, 0x00, 0x00, 0x50, 0x00, 0x00, 0x00,
]))
# image size,
# flag: Point Sampling | No Mipmaps | No Level Of Detail | Eight Bit Alpha
# number of frames (1 for no animation), first frame of animation
vtf.write(bytes([
*i16(width), *i16(height), 0x01, 0x23, 0x00, 0x00,
*i16(len(raws)), *i16(0), 0x00, 0x00, 0x00, 0x00,
*f32(sX), *f32(sY),
*f32(sZ), 0x00, 0x00, 0x00, 0x00,
]))
# bump scale (1.0f), image format: IMAGE_FORMAT_RGBA8888(0) or IMAGE_FORMAT_RGB888(2)
# mipcount, no low res image, depth, for 7.2+
vtf.write(bytes([
0x00, 0x00, 0x80, 0x3F, *i32(0 if mode == 'RGBA' else 2),
0x01, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x01,
0x00
]))
# padding
vtf.write(bytes(80 - vtf.tell()))
# image data
for raw in raws:
vtf.write(raw)
vtf.close()
QC_MODELBASE = """$cdmaterials "AMC/"
$ambientboost
$scale 2.28125
$staticprop
"""
QC_HEADER = """// Template header
$definevariable mdlname "{model_file}"
$include "../modelbase_1.qci"
"""
QC_FOOTER = """
// Block Template
$surfaceprop "default"
$keyvalues
{
prop_data
{
"base" "Plastic.Medium"
}
}
$modelname AMC/$mdlname$.mdl
$model "Body" $mesh$
$applytextures // Call macro
$sequence idle $mesh$.smd loop fps 1.00
$collisionmodel $mesh$.smd
{
$concave
$mass 50.0
}
"""
VMT_MODELS_TEMPLATE = """VertexLitGeneric
{{
"$basetexture" "{cdmaterials}{textire_file}"
"$surfaceprop" "{surfaceprop}"
"$alphatest" "1"
{proxy}
}}
"""
VMT_PROXY_ANIMATION = """
Proxies
{{
AnimatedTexture
{{
animatedtexturevar $basetexture
animatedtextureframenumvar $frame
animatedtextureframerate {frametime}
}}
}}
"""
missing_texture = ('iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAAIGNIUk0AAHol'
+ 'AACAgwAA+f8AAIDpAAB1MAAA6mAAADqYAAAXb5JfxUYAAAAjSURBVCjPY/zD8J8BG2BhYMQqzjiq'
+ 'gSYaGHAAXAaNaqCJBgBNyh/pMWe+mgAAAABJRU5ErkJggg==')
class LineBuilder:
def __init__(self):
self.str = ''
def __enter__(self):
return self
def __exit__(self, type, value, trace):
pass
def __call__(self, text='', end='\n'):
self.str += text + end
def __iadd__(self, text):
self.str += text
return self
def __str__(self):
return self.str
def __len__(self):
return len(self.str)
class Vector:
def __init__(self, x=0, y=0, z=0):
self.x = float(x)
self.y = float(y)
self.z = float(z)
def dot(self, other) -> float:
return self.x*other.x + self.y*other.y + self.z*other.z
def __abs__(self):
return math.sqrt(self.x**2 + self.y**2 + self.z**2)
def __pos__(self):
return Vector(self.x, self.y, self.z)
def __neg__(self):
return Vector(-self.x, -self.y, -self.z)
def __eq__(self, other):
return abs(self - other) <= 1e-6
def __ne__(self, other):
return abs(self - other) > 1e-6
def __add__(self, other):
if isinstance(other, Vector):
return Vector(self.x + other.x, self.y + other.y, self.z + other.z)
else:
return Vector(self.x + other, self.y + other, self.z + other)
def __sub__(self, other):
if isinstance(other, Vector):
return Vector(self.x - other.x, self.y - other.y, self.z - other.z)
else:
return Vector(self.x - other, self.y - other, self.z - other)
def __mul__(self, other):
if isinstance(other, Vector):
return Vector(self.x * other.x, self.y * other.y, self.z * other.z)
else:
return Vector(self.x * other, self.y * other, self.z * other)
def __div__(self, other):
if isinstance(other, Vector):
return Vector(self.x / other.x, self.y / other.y, self.z / other.z)
else:
return Vector(self.x / other, self.y / other, self.z / other)
def __truediv__(self, other):
if isinstance(other, Vector):
return Vector(self.x / other.x, self.y / other.y, self.z / other.z)
else:
return Vector(self.x / other, self.y / other, self.z / other)
def __radd__(self, other):
if isinstance(other, Vector):
return Vector(other.x + self.x, other.y + self.y, other.z + self.z)
else:
return Vector(other + self.x, other + self.y, other + self.z)
def __rsub__(self, other):
if isinstance(other, Vector):
return Vector(other.x - self.x, other.y - self.y, other.z - self.z)
else:
return Vector(other - self.x, other - self.y, other - self.z)
def __rmul__(self, other):
if isinstance(other, Vector):
return Vector(other.x * self.x, other.y * self.y, other.z * self.z)
else:
return Vector(other * self.x, other * self.y, other * self.z)
def __rdiv__(self, other):
if isinstance(other, Vector):
return Vector(other.x / self.x, other.y / self.y, other.z / self.z)
else:
return Vector(other / self.x, other / self.y, other / self.z)
def __rtruediv__(self, other):
if isinstance(other, Vector):
return Vector(other.x / self.x, other.y / self.y, other.z / self.z)
else:
return Vector(other / self.x, other / self.y, other / self.z)
def __iadd__(self, other):
if isinstance(other, Vector):
self.x += other.x
self.y += other.y
self.z += other.z
else:
self.x += other
self.y += other
self.z += other
return self
def __isub__(self, other):
if isinstance(other, Vector):
self.x -= other.x
self.y -= other.y
self.z -= other.z
else:
self.x -= other
self.y -= other
self.z -= other
return self
def __imul__(self, other):
if isinstance(other, Vector):
self.x *= other.x
self.y *= other.y
self.z *= other.z
else:
self.x *= other
self.y *= other
self.z *= other
return self
def __idiv__(self, other):
if isinstance(other, Vector):
self.x /= other.x
self.y /= other.y
self.z /= other.z
else:
self.x /= other
self.y /= other
self.z /= other
return self
def __itruediv__(self, other):
if isinstance(other, Vector):
self.x /= other.x
self.y /= other.y
self.z /= other.z
else:
self.x /= other
self.y /= other
self.z /= other
return self
def __getitem__(self, key):
if not isinstance(key, int):
raise KeyError(f'key: {key} must be interger')
if key == 0:
return self.x
elif key == 1:
return self.y
elif key == 2:
return self.z
else:
raise KeyError(f'key: {key} dose not exists')
def __setitem__(self, key, val):
if not isinstance(key, int):
raise KeyError(f'key: {key} must be interger')
if key == 0:
self.x = val
elif key == 1:
self.y = val
elif key == 2:
self.z = val
else:
raise KeyError(f'key: {key} dose not exists')
def __str__(self):
return f'{self.x:.5f} {self.y:.5f} {self.z:.5f}'
def __repr__(self):
return f'{self.x:.1f} {self.y:.1f} {self.z:.1f}'
def rotate_x(self, angle):
rad = math.radians(angle)
cos = math.cos(rad)
sin = math.sin(rad)
return Vector(self.x, cos*self.y-sin*self.z, sin*self.y+cos*self.z)
def rotate_y(self, angle):
rad = math.radians(angle)
cos = math.cos(rad)
sin = math.sin(rad)
return Vector(cos*self.x + sin*self.z, self.y, -sin*self.x + cos*self.z)
def rotate_z(self, angle):
rad = math.radians(angle)
cos = math.cos(rad)
sin = math.sin(rad)
return Vector(cos*self.x - sin*self.y, sin*self.x + cos*self.y, self.z)
def rotate(self, angle, center=None):
vec = Vector(self.x, self.y, self.z)
if angle[0] != 0:
if center is not None:
vec -= center
vec = vec.rotate_x(angle[0])
if center is not None:
vec += center
if angle[1] != 0:
if center is not None:
vec -= center
vec = vec.rotate_y(angle[1])
if center is not None:
vec += center
if angle[2] != 0:
if center is not None:
vec -= center
vec = vec.rotate_z(angle[2])
if center is not None:
vec += center
return vec
class Vertex:
def __init__(self, pos: Vector, uv: Vector):
self.pos = pos
self.uv = uv
def translate(self, vec: Vector):
self.pos += vec
def scale(self, scale: Vector):
self.pos *= scale
def rotate(self, angle: Vector, center=None):
self.pos = self.pos.rotate(angle, center)
def to_smd(self, normal: Vector) -> str:
# <int|Parent bone> <float|PosX PosY PosZ> <normal|NormX NormY NormZ> <normal|U V> [ignores]
return f'0 {self.pos} {normal} {self.uv.x:.5f} {self.uv.y:.5f}'
class Face:
def __init__(self, texture: str, vertices: List[Vertex], norm: Vector):
self.tex = texture
self.vert = vertices
self.norm = norm
def translate(self, vec: Vector):
for v in self.vert:
v.translate(vec)
def scale(self, scale: Vector):
for v in self.vert:
v.scale(scale)
def rotate(self, angle: Vector, center=None):
for v in self.vert:
v.rotate(angle, center)
def to_smd(self) -> str:
vert = self.vert
builder = self.tex + '\n'
builder += vert[0].to_smd(self.norm) + '\n'
builder += vert[1].to_smd(self.norm) + '\n'
builder += vert[2].to_smd(self.norm) + '\n'
builder += self.tex + '\n'
builder += vert[1].to_smd(self.norm) + '\n'
builder += vert[3].to_smd(self.norm) + '\n'
builder += vert[2].to_smd(self.norm) + '\n'
return builder
class Cube:
def __init__(self, position: Vector, size: Vector):
self.size = size
self.position = position
self.faces = []
def add_face(self, face: int, texture: str, uv: List[Vector]):
if face == 0: # east
norm = Vector(-1.0, 0.0, 0.0)
vertices = [
Vertex(Vector(0.0, 1.0, 1.0), uv[0]),
Vertex(Vector(0.0, 1.0, 0.0), uv[1]),
Vertex(Vector(0.0, 0.0, 1.0), uv[2]),
Vertex(Vector(0.0, 0.0, 0.0), uv[3]),
]
elif face == 1: # west
norm = Vector(1.0, 0.0, 0.0)
vertices = [
Vertex(Vector(1.0, 0.0, 1.0), uv[0]),
Vertex(Vector(1.0, 0.0, 0.0), uv[1]),
Vertex(Vector(1.0, 1.0, 1.0), uv[2]),
Vertex(Vector(1.0, 1.0, 0.0), uv[3]),
]
elif face == 2: # up
norm = Vector(0.0, 0.0, 1.0)
vertices = [
Vertex(Vector(0.0, 1.0, 1.0), uv[0]),
Vertex(Vector(0.0, 0.0, 1.0), uv[1]),
Vertex(Vector(1.0, 1.0, 1.0), uv[2]),
Vertex(Vector(1.0, 0.0, 1.0), uv[3]),
]
elif face == 3: # down
norm = Vector(0.0, 0.0, -1.0)
vertices = [
Vertex(Vector(0.0, 0.0, 0.0), uv[0]),
Vertex(Vector(0.0, 1.0, 0.0), uv[1]),
Vertex(Vector(1.0, 0.0, 0.0), uv[2]),
Vertex(Vector(1.0, 1.0, 0.0), uv[3]),
]
elif face == 4: # south
norm = Vector(0.0, 1.0, 0.0)
vertices = [
Vertex(Vector(1.0, 1.0, 1.0), uv[0]),
Vertex(Vector(1.0, 1.0, 0.0), uv[1]),
Vertex(Vector(0.0, 1.0, 1.0), uv[2]),
Vertex(Vector(0.0, 1.0, 0.0), uv[3]),
]
elif face == 5: # north
norm = Vector(0.0, -1.0, 0.0)
vertices = [
Vertex(Vector(0.0, 0.0, 1.0), uv[0]),
Vertex(Vector(0.0, 0.0, 0.0), uv[1]),
Vertex(Vector(1.0, 0.0, 1.0), uv[2]),
Vertex(Vector(1.0, 0.0, 0.0), uv[3]),
]
# reference cube is center @ [0.5, 0.5, 0.5]
face = Face(texture, vertices, norm)
face.translate(Vector(-0.5, -0.5, -0.5))
face.scale(self.size)
face.translate(self.position)
self.faces += [face]
def translate(self, vec: Vector):
for face in self.faces:
face.translate(vec)
def scale(self, scale):
for face in self.faces:
face.scale(scale)
def rotate(self, angle: Vector, center=None, rescale=False):
if center is not None:
self.translate(-center)
for face in self.faces:
face.rotate(angle)
if rescale:
scaled = self.position.rotate(angle)
scale = self.position.dot(
self.position) / self.position.dot(scaled) # porjection
self.scale(Vector(scale, scale, 1))
if center is not None:
self.translate(center)
def to_smd(self) -> str:
builder = ''
for face in self.faces:
builder += face.to_smd()
return builder
def to_smdbones(self, center: Vector) -> str:
return f'{center} 0.00000 0.00000 0.00000'
class SMDModel:
def __init__(self):
self.textures = {}
self.cubes = []
self.min = Vector()
self.max = Vector()
def add_cube(self, position: Vector, scale: Vector) -> Cube:
cube = Cube(position, scale)
self.cubes += [cube]
return cube
def translate(self, vec: Vector):
for cube in self.cubes:
cube.translate(vec)
def to_smd(self) -> str:
builder = ''
for e in self.cubes:
builder += e.to_smd()
return builder
def to_smdbones(self) -> str:
builder = ''
for i, e in enumerate(self.cubes):
# assume it center at 0
builder += f'{i} {e.to_smdbones(Vector())}\n'
return builder
def export_texture(texture: str, texture_dir: str, out_dir: str) -> None:
logger.info(f'Exporting texture: {texture}')
out = os.path.normpath(os.path.join(out_dir, texture))
if texture == '$missing':
im = Image.open(io.BytesIO(base64.b64decode(missing_texture)))
else:
source = os.path.normpath(os.path.join(texture_dir, texture + '.png'))
if not os.path.exists(source):
logger.error(f'Texture file dose not exists: {source}')
exit(1)
im = Image.open(source)
if im.format != 'PNG':
logger.warning(f'Source texture is not PNG: {texture}')
if im.mode != 'RGB' or im.mode != 'RGBA':
im = im.convert('RGBA')
meta = os.path.normpath(os.path.join(texture_dir, texture + '.png.mcmeta'))
if os.path.exists(meta):
with open(meta) as f:
animation = json.load(f)['animation']
# TODO:
if 'frames' in animation:
logger.warning(
f'texture {texture} has "animation.frames", but was currently not supported')
# minecraft animation 20 fps, it might cause some lag in csgo
if 'frametime' in animation:
frametime = 20 / animation['frametime']
else:
frametime = 20
# slow it down or fps will drop when player up close
frametime /= 2
frame_size = im.width
assert im.height % frame_size == 0, f'invalid animated texture: {texture}'
proxy = VMT_PROXY_ANIMATION.format(frametime=frametime)
textureDimens[texture] = Vector(frame_size, frame_size)
save_vtf([
im.crop((0, i * frame_size, frame_size, i * frame_size + frame_size))
for i in range(0, im.height // frame_size)
], out + '.vtf')
else:
proxy = ''
textureDimens[texture] = Vector(*im.size)
save_vtf(im, out + '.vtf')
cdmaterials = 'AMC/'
with open(out + '.vmt', 'w') as f:
f.write(VMT_MODELS_TEMPLATE.format(
cdmaterials=cdmaterials, textire_file=texture, surfaceprop='', proxy=proxy))
game_material = os.path.join(game_path, 'materials', cdmaterials, texture)
shutil.copyfile(out + '.vmt', game_material + '.vmt')
shutil.copyfile(out + '.vtf', game_material + '.vtf')
def resolve_texture(texture: str) -> str:
if textureVars[texture][0] == '#':
return resolve_texture(textureVars[texture][1:])
return textureVars[texture]
def resolve_uv(texture: str) -> str:
tex_file = resolve_texture(texture)
if tex_file not in textureDimens:
export_texture(tex_file, textures_path, out_textures_path)
return tex_file
def convert_uv(mcuv: List[float], rotation=0) -> List[Vector]:
mcuv = [v / 16 for v in mcuv]
ref = [
(mcuv[0], 1 - mcuv[3]),
(mcuv[0], 1 - mcuv[1]),
(mcuv[2], 1 - mcuv[3]),
(mcuv[2], 1 - mcuv[1]),
]
# source uv coordinate
uv = [
Vector(ref[1][0], ref[1][1]),
Vector(ref[0][0], ref[0][1]),
Vector(ref[3][0], ref[3][1]),
Vector(ref[2][0], ref[2][1]),
]
for i in range(rotation // 90):
uv = [uv[1], uv[3], uv[0], uv[2]]
return uv
def parse_model(models_path: str, model_file: str) -> List[str]:
# TODO: handle namespace
model_file = model_file.replace('minecraft:', '')
with open(os.path.join(models_path, model_file + '.json')) as f:
jmodel = json.load(f)
idx = model_file.find('/')
model_name = model_file[idx + 1:]
logger.info(f'new qc: ' + model_file)
qc = LineBuilder()
textures = []
undefined_textures = []
if 'textures' in jmodel:
qc('// JSON "textures":')
for tex, val in jmodel['textures'].items():
if ':' in val:
val = val.split(':')[1]
if val[0] == '#':
# imported texture
undefined_textures += [val[1:]]
qc(f'$definevariable texture_{tex} $texture_{val[1:]}$')
else:
# real texture
val = val.replace('minecraft:', '')
qc(f'$definevariable texture_{tex} "{val}"')
export_texture(val, textures_path, out_textures_path)
# defined texture variable
textures += [tex]
assert tex not in textureVars, f'Texture varibale "{tex}" redefined'
textureVars[tex] = val
qc()
if 'parent' in jmodel:
parent_file = jmodel['parent']
undefined_textures += parse_model(models_path, parent_file)
idx = parent_file.find('/')
qc(f'// JSON "parent":')
qc(f'$include "{parent_file[idx + 1:]}.qci"')
qc()
if 'elements' in jmodel:
qc(f'// JSON "elements"')
qc(f'$definevariable mesh {model_name}')
qc()
model = SMDModel()
model_textures = []
for elem in jmodel['elements']:
start = Vector(*elem['from'])
end = Vector(*elem['to'])
# the "height" of Source engine should be z
start[0], start[1], start[2] = start[0], start[2], start[1]
end[0], end[1], end[2] = end[0], end[2], end[1]
size = end - start
postiton = start + size / 2
postiton = (postiton) * Vector(-1, 1, 1)
cube = model.add_cube(postiton, size)
logger.info(f'cube: {size} @ {postiton}')
for facename, face in elem['faces'].items():
texture = face['texture']
if texture[0] != '#':
raise Exception(
'expect face texture to be a texture variable')
# TODO: handle overlay texture, maybe by proxy
if texture == '#overlay':
continue
texture = texture[1:]
if texture not in model_textures:
model_textures += [texture]
if texture not in textureVars:
logger.warning(
f'texture variable "{texture}" was undefined, the model {model_file} might be template file')
if not args.allow_template:
logger.error(f'no missing texture was allowed, exiting')
exit(1)
else:
textureVars[texture] = '$missing'
logger.debug(f'facename: {facename} -> {resolve_uv(texture)}')
rotation = 0
if 'rotation' in face:
rotation = face['rotation']
uv = None
if 'uv' not in face:
if facename in ['east', 'west']: # yz plane
uv = [start.y, start.z, end.y, end.z]
elif facename in ['up', 'down']: # xy plane
uv = [start.x, start.y, end.x, end.y]
elif facename in ['south', 'north']: # xz plane
uv = [start.x, start.z, end.x, end.z]
else:
uv = face['uv']
uv = convert_uv(uv, rotation)
# we need to rotate "up" and "down" face to match minecraft
if facename in ['up', 'down']:
uv = [uv[3], uv[2], uv[1], uv[0]]
cube_faces = {
"east": 0,
"west": 1,
"up": 2,
"down": 3,
"south": 4,
"north": 5,
}
cube.add_face(cube_faces[facename], f'@{texture}', uv)
# now model center at (0, 0, 8), the buttom is on the ground
cube.translate(Vector(8, -8, 0))
if 'rotation' in elem:
axis = elem['rotation']['axis']
_angle = elem['rotation']['angle']
if 'origin' in elem['rotation']:
origin = Vector(*elem['rotation']['origin'])
else:
origin = Vector(8, 8, 8)
rescale = False
if 'rescale' in elem['rotation']:
rescale = elem['rotation']['rescale']
angle = Vector()
angle[0 if axis == 'x' else 1 if axis == 'y' else 2] = _angle
angle[0], angle[1], angle[2] = -angle[0], angle[2], angle[1]
origin[1], origin[2] = origin[2], origin[1]
origin = origin - Vector(8, 8, 0)
cube.rotate(angle, origin * Vector(-1, 1, 1), rescale)
qc(f'// JSON "elements[].faces[].texture" list:')
# use macro to replace texture variable
qc(f'$definemacro applytextures \\\\')
for tex in set(model_textures):
if tex not in textures:
undefined_textures += [tex]
qc(f'$renamematerial "@{tex}" $texture_{tex}$ \\\\')
qc()
logger.info(f'New smd: {model_file}')
with open(os.path.join(out_models_path, model_file + '.smd'), 'w', encoding='utf-8') as f:
smd = LineBuilder()
smd('version 1')
smd('nodes')
smd('000 "Cube" -1')
smd('end')
smd('skeleton')
smd('time 0')
smd(model.to_smdbones(), end='')
smd('end')
smd('triangles')
smd(model.to_smd(), end='')
smd('end')
f.write(str(smd))
require_textures = []
for tex in undefined_textures:
if tex not in textures:
require_textures += [tex]
real_model = len(require_textures) == 0 and len(textures) > 0
qc_ext = '.qc' if real_model else '.qci'
with open(os.path.join(out_models_path, model_file + qc_ext), 'w', encoding='utf-8') as f:
if real_model:
f.write(QC_HEADER.format(model_file=model_file))
f.write(str(qc))
if real_model:
f.write(QC_FOOTER)
return require_textures
parser = argparse.ArgumentParser(
description='Convert MineCraft JSON model to Source engine model')
parser.add_argument('model', type=str,
help='minecraft model, e.g. "block/furnace_on"')
parser.add_argument('--tools', type=str, help='the folder contains studiomdl.exe',
default=r'C:\Program Files (x86)\Steam\steamapps\common\Counter-Strike Global Offensive\bin')
parser.add_argument('--game', type=str, help='the folder contains gameinfo.txt',
default=r'C:\Program Files (x86)\Steam\steamapps\common\Counter-Strike Global Offensive\csgo')
parser.add_argument('--assets', type=str,
help='path to minecraft assets folder')
parser.add_argument('-o', '--out', type=str,
help='output folder', default='csgo2')
parser.add_argument('--allow-template', type=bool,
action='store_true', default=False)
args = parser.parse_args()
tools_path = args.tools
game_path = args.game
assets_path = args.assets
textures_path = os.path.join(assets_path, 'textures')
models_path = os.path.join(assets_path, 'models')
out_dir = args.out
out_models_path = os.path.join(out_dir, 'modelsrc/')
out_textures_path = os.path.join(out_dir, 'materialsrc/')
model_base = os.path.join(out_models_path, 'modelbase_1.qci')
if os.path.exists(model_base):
logger.info('creating modelbase_1.qci')
os.makedirs(out_models_path, exist_ok=True)
with open(model_base, 'w', encoding='utf-8') as f:
print(QC_MODELBASE, file=f)
# TODO: dont use global variables
textureDimens = {}
textureVars = {}
json_model = args.json_model
parse_model(models_path, json_model)
cmd = f'"{os.path.join(tools_path, "studiomdl.exe")}" -game "{game_path}" -nop4 -nox360 "{os.path.join(out_models_path, json_model)}.qc"'
logger.debug(cmd)
ok, _, stdout = runcmd(cmd)
if not ok:
logger.error(stdout.decode())
@Csor777
Copy link

Csor777 commented Nov 11, 2024

How to use it?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment