Last active
December 3, 2023 11:05
-
-
Save Kas-tle/1ceffdcef37aaaeffe938d97ec522ea5 to your computer and use it in GitHub Desktop.
Java to Bedrock 3D Model Converter 1.1
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env bash | |
: ${1?'Please specify an input resource pack in the same directory as the script (e.g. ./converter.sh MyResourcePack.zip)'} | |
# define color placeholders | |
C_RED='\e[31m' | |
C_GREEN='\e[32m' | |
C_YELLOW='\e[33m' | |
C_BLUE='\e[36m' | |
C_GRAY='\e[37m' | |
C_CLOSE='\e[m' | |
# status message function | |
status_message () { | |
case $1 in | |
"completion") | |
printf "${C_GREEN}[+] ${C_GRAY}${2}${C_CLOSE}\n" | |
;; | |
"process") | |
printf "${C_YELLOW}[•] ${C_GRAY}${2}${C_CLOSE}\n" | |
;; | |
"critical") | |
printf "${C_RED}[X] ${C_GRAY}${2}${C_CLOSE}\n" | |
;; | |
"error") | |
printf "${C_RED}[ERROR] ${C_GRAY}${2}${C_CLOSE}\n" | |
;; | |
"info") | |
printf "${C_BLUE}${2}${C_CLOSE}\n" | |
;; | |
"plain") | |
printf "${C_GRAY}${2}${C_CLOSE}\n" | |
;; | |
esac | |
} | |
dependency_check () { | |
if command ${3} 2>/dev/null | grep -q "${4}"; then | |
status_message completion "Dependency ${1} satisfied" | |
else | |
status_message error "Dependency ${1} must be installed to proceed\nSee ${2}\nExiting script..." | |
exit 1 | |
fi | |
} | |
user_input () { | |
status_message plain "${2} ${C_YELLOW}[${3}]\n" | |
read -p "${4}: " ${1} | |
echo | |
} | |
# ensure input pack exists | |
if ! test -f "${1}"; then | |
status_message error "Input resource pack ${1} is not in this directory" | |
exit 1 | |
else | |
status_message process "Input file ${1} detected" | |
fi | |
printf '\e[1;31m%-6s\e[m\n' " | |
███████████████████████████████████████████████████████████████████████████████ | |
████████████████████████ # <!> # W A R N I N G # <!> # ████████████████████████ | |
███████████████████████████████████████████████████████████████████████████████ | |
███ This script has been provided as is. If your resource pack does not ███ | |
███ entirely conform the vanilla resource specification, including but not ███ | |
███ limited to, missing textures, improper parenting, improperly defined ███ | |
███ predicates, and malformed JSON files, among other problems, there is a ███ | |
███ strong possibility this script will fail. Please remedy any potential ███ | |
███ resource pack formatting errors before attempting to make use of this ███ | |
███ converter. You have been warned. ███ | |
███████████████████████████████████████████████████████████████████████████████ | |
███████████████████████████████████████████████████████████████████████████████ | |
███████████████████████████████████████████████████████████████████████████████ | |
" | |
read -p $'\e[37mTo acknowledge and continue, press enter. To exit, press Ctrl+C.:\e[0m | |
' | |
# ensure we have all the required dependencies | |
dependency_check "jq-1.6" "https://stedolan.github.io/jq/download/" "jq --version" "1.6" | |
dependency_check "sponge" "https://joeyh.name/code/moreutils/" "-v sponge" "" | |
dependency_check "imagemagick-7" "https://imagemagick.org/script/download.php" "magick --version" "ImageMagick 7." | |
dependency_check "spritesheet-js" "https://www.npmjs.com/package/spritesheet-js" "-v spritesheet-js" "" | |
status_message completion "All dependencies have been satisfied\n" | |
# initial configuration | |
if [[ ${2} != default ]] | |
then | |
status_message info "This script will now ask some configuration questions. Default values are yellow. Simply press enter to use the defaults.\n" | |
user_input merge_input "Is there an existing bedrock pack in this directory with which you would like the output merged? (e.g. input.mcpack)" "null" "Input pack to merge" | |
user_input attachable_material "What material should we use for the attachables?" "entity_alphatest" "Attachable material" | |
user_input block_material "What material should we use for the blocks?" "alpha_test" "Block material" | |
user_input fallback_pack "From what URL should we download the fallback resource pack? (must be a direct link)\n Use 'none' if default resources are not needed." "null" "Fallback pack URL" | |
fi | |
status_message plain " | |
Generating Bedrock 3D resource pack with settings: | |
${C_GRAY}Input pack to merge: ${C_BLUE}${merge_input:=null} | |
${C_GRAY}Attachable material: ${C_BLUE}${attachable_material:=entity_alphatest} | |
${C_GRAY}Block material: ${C_BLUE}${block_material:=alpha_test} | |
${C_GRAY}Fallback pack URL: ${C_BLUE}${fallback_pack:=null} | |
" | |
# decompress our input pack | |
status_message process "Decompressing input pack" | |
unzip -q ${1} | |
status_message completion "Input pack decompressed" | |
# get the current default textures and merge them with our rp | |
if [[ ${fallback_pack} != none ]] | |
then | |
status_message process "Now downloading the fallback resource pack:" | |
fi | |
if [[ ${fallback_pack} = null ]] | |
then | |
printf "\e[3m\e[37m" | |
wget -nv --show-progress -O default_assets.zip https://github.com/InventivetalentDev/minecraft-assets/zipball/refs/tags/1.18.2 | |
printf "${C_CLOSE}" | |
status_message completion "Fallback resources downloaded" | |
root_folder=($(unzip -Z -1 default_assets.zip | head -1)) | |
fi | |
if [[ ${fallback_pack} != null && ${fallback_pack} != none ]] | |
then | |
printf "\e[3m\e[37m" | |
wget -nv --show-progress -O default_assets.zip "${fallback_pack}" | |
status_message completion "Fallback resources downloaded" | |
root_folder="" | |
fi | |
if [[ ${fallback_pack} != none ]] | |
then | |
mkdir ./defaultassetholding | |
unzip -q -d ./defaultassetholding default_assets.zip "${root_folder}assets/minecraft/textures/**/*" | |
status_message completion "Fallback resources decompressed" | |
cp -n -r "./defaultassetholding/${root_folder}assets/minecraft/textures"/* './assets/minecraft/textures/' | |
status_message completion "Fallback resources merged with target pack" | |
rm -rf defaultassetholding | |
rm -f default_assets.zip | |
status_message critical "Extraneous fallback resources deleted\n" | |
fi | |
# generate a fallback texture | |
convert -size 16x16 xc:\#FFFFFF ./assets/minecraft/textures/0.png | |
# setup our initial config | |
status_message process "Iterating through all vanilla associated model JSONs to generate initial predicate config\nOn a large pack, this may take some time...\n" | |
if test -d "./assets/minecraft/models/item"; then confarg1="./assets/minecraft/models/item/*.json"; fi | |
if test -d "./assets/minecraft/models/block"; then confarg2="./assets/minecraft/models/block/*.json"; fi | |
jq -n '[inputs | {(input_filename | sub("(.+)/(?<itemname>.*?).json"; .itemname)): .overrides?[]?}] | | |
def maxdur($input): | |
({ | |
"carrot_on_a_stick": 25, | |
"golden_axe": 32, | |
"golden_hoe": 32, | |
"golden_pickaxe": 32, | |
"golden_shovel": 32, | |
"golden_sword": 32, | |
"wooden_axe": 59, | |
"wooden_hoe": 59, | |
"wooden_pickaxe":59, | |
"wooden_shovel":59, | |
"wooden_sword": 59, | |
"fishing_rod": 64, | |
"flint_and_steel": 64, | |
"warped_fungus_on_a_stick": 100, | |
"sparkler": 100, | |
"glow_stick": 100, | |
"stone_axe": 131, | |
"stone_hoe": 131, | |
"stone_pickaxe":131, | |
"stone_shovel":131, | |
"stone_sword": 131, | |
"shears": 238, | |
"iron_axe": 250, | |
"iron_hoe": 250, | |
"iron_pickaxe": 250, | |
"iron_shovel": 250, | |
"iron_sword": 250, | |
"trident": 250, | |
"crossbow": 326, | |
"shield": 336, | |
"bow": 384, | |
"elytra": 432, | |
"diamond_axe": 1561, | |
"diamond_hoe": 1561, | |
"diamond_pickaxe": 1561, | |
"diamond_shovel": 1561, | |
"diamond_sword": 1561, | |
"netherite_axe": 2031, | |
"netherite_hoe": 2031, | |
"netherite_pickaxe": 2031, | |
"netherite_shovel": 2031, | |
"netherite_sword": 2031 | |
} | .[$input] // 1) | |
; | |
def namespace: | |
if contains(":") then sub("\\:(.+)"; "") else "minecraft" end | |
; | |
[.[] | to_entries | map( select((.value.predicate.damage != null) or (.value.predicate.damaged != null) or (.value.predicate.custom_model_data != null)) | | |
(if .value.predicate.damage then (.value.predicate.damage * maxdur(.key) | round) else null end) as $damage | |
| (if .value.predicate.damaged == 0 then 1 else null end) as $unbreakable | |
| (if .value.predicate.custom_model_data then .value.predicate.custom_model_data else null end) as $custom_model_data | | |
{ | |
"item": .key, | |
"nbt": ({ | |
"Damage": $damage, | |
"Unbreakable": $unbreakable, | |
"CustomModelData": $custom_model_data | |
}), | |
"path": ("./assets/" + (.value.model | namespace) + "/models/" + (.value.model | sub("(.*?)\\:"; "")) + ".json") | |
}) | .[]] | |
| walk(if type == "object" then with_entries(select(.value != null)) else . end) | |
| to_entries | map( ((.value.geyserID = "gmdl_\(1+.key)" | .value.geometry = ("geometry.geysercmd." + "gmdl_\(1+.key)")) | .value)) | |
| INDEX(.geyserID) | |
' ${confarg1} ${confarg2} | sponge config.json | |
status_message completion "Initial predicate config generated" | |
# get a bash array of all model json files in our resource pack | |
status_message process "Generating an array of all model JSON files to crosscheck with our predicate config" | |
json_dir=($(find ./assets/**/models -type f -name '*.json')) | |
# ensure all our reference files in config.json exist, and delete the entry if they do not | |
status_message critical "Removing config entries that do not have an associated JSON file in the pack" | |
jq ' | |
def real_file($input): | |
($ARGS.positional | index($input) // null); | |
map_values(if real_file(.path) != null then . else empty end) | |
' config.json --args ${json_dir[@]} | sponge config.json | |
# get a bash array of all our input models | |
status_message process "Creating a bash array for remaing models in our predicate config" | |
model_array=($(jq -r '.[].path' config.json)) | |
# find initial parental information | |
status_message process "Doing an initial sweep for level 1 parentals" | |
jq -n ' | |
[def namespace: if contains(":") then sub("\\:(.+)"; "") else "minecraft" end; | |
inputs | { | |
"path": (input_filename), | |
"parent": ("./assets/" + (.parent | namespace) + "/models/" + ((.parent? // empty) | sub("(.*?)\\:"; "")) + ".json") | |
} | |
] | |
' ${model_array[@]} | sponge parents.json | |
# add initial parental information to config.json | |
status_message critical "Removing config entries with non-supported parentals\n" | |
jq -s ' | |
. as $global | | |
def intest($input_i): ($global | .[0] | map({(.path): .parent}) | add | .[$input_i]? // null); | |
def gtest($input_g): | |
["./assets/minecraft/models/block/block.json", "./assets/minecraft/models/block/cube.json", "./assets/minecraft/models/block/cube_column.json", "./assets/minecraft/models/block/cube_directional.json", "./assets/minecraft/models/block/cube_mirrored.json", "./assets/minecraft/models/block/observer.json", "./assets/minecraft/models/block/orientable_with_bottom.json", "./assets/minecraft/models/block/piston_extended.json", "./assets/minecraft/models/block/redstone_dust_side.json", "./assets/minecraft/models/block/redstone_dust_side_alt.json", "./assets/minecraft/models/block/template_single_face.json", "./assets/minecraft/models/block/thin_block.json", "./assets/minecraft/models/builtin/entity.json", "./assets/minecraft/models/builtin/generated.json", "./assets/minecraft/models/item/bow.json", "./assets/minecraft/models/item/chest.json", "./assets/minecraft/models/item/crossbow.json", "./assets/minecraft/models/item/fishing_rod.json", "./assets/minecraft/models/item/generated.json", "./assets/minecraft/models/item/handheld.json", "./assets/minecraft/models/item/handheld_rod.json", "./assets/minecraft/models/item/template_skull.json"] | |
| index($input_g) // null; | |
.[1] | map_values(. + ({"parent": (intest(.path) // null)} | if gtest(.parent) == null then . else empty end)) | |
| walk(if type == "object" then with_entries(select(.value != null)) else . end) | |
' parents.json config.json | sponge config.json | |
# create our initial directories for bp & rp | |
status_message process "Generating initial directory strucutre for our bedrock packs" | |
mkdir -p ./target/rp/models/blocks/geysercmd && mkdir -p ./target/rp/textures/blocks/geysercmd && mkdir -p ./target/rp/attachables/geysercmd && mkdir -p ./target/rp/animations/geysercmd && mkdir -p ./target/bp/blocks/geysercmd | |
# copy over our pack.png if we have one | |
if test -f "./pack.png"; then | |
cp ./pack.png ./target/rp/pack_icon.png && cp ./pack.png ./target/bp/pack_icon.png | |
fi | |
# generate uuids for our manifests | |
uuid1=($(uuidgen)) | |
uuid2=($(uuidgen)) | |
uuid3=($(uuidgen)) | |
uuid4=($(uuidgen)) | |
# get pack description if we have one | |
pack_desc=($(jq -r '(.pack.description // "Geyser 3D Items Resource Pack")' ./pack.mcmeta)) | |
# generate rp manifest.json | |
status_message process "Generating resource pack manifest" | |
jq -c --arg pack_desc "${pack_desc}" --arg uuid1 "${uuid1}" --arg uuid2 "${uuid2}" -n ' | |
{ | |
"format_version": 2, | |
"header": { | |
"description": "Adds 3D items for use with a Geyser proxy", | |
"name": $pack_desc, | |
"uuid": ($uuid1 | ascii_downcase), | |
"version": [1, 0, 0], | |
"min_engine_version": [1, 16, 100] | |
}, | |
"modules": [ | |
{ | |
"description": "Adds 3D items for use with a Geyser proxy", | |
"type": "resources", | |
"uuid": ($uuid2 | ascii_downcase), | |
"version": [1, 0, 0] | |
} | |
] | |
} | |
' | sponge ./target/rp/manifest.json | |
# generate bp manifest.json | |
status_message process "Generating behavior pack manifest" | |
jq -c --arg pack_desc "${pack_desc}" --arg uuid1 "${uuid1}" --arg uuid3 "${uuid3}" --arg uuid4 "${uuid4}" -n ' | |
{ | |
"format_version": 2, | |
"header": { | |
"description": "Adds 3D items for use with a Geyser proxy", | |
"name": $pack_desc, | |
"uuid": ($uuid3 | ascii_downcase), | |
"version": [1, 0, 0], | |
"min_engine_version": [ 1, 16, 100] | |
}, | |
"modules": [ | |
{ | |
"description": "Adds 3D items for use with a Geyser proxy", | |
"type": "data", | |
"uuid": ($uuid4 | ascii_downcase), | |
"version": [1, 0, 0] | |
} | |
], | |
"dependencies": [ | |
{ | |
"uuid": ($uuid1 | ascii_downcase), | |
"version": [1, 0, 0] | |
} | |
] | |
} | |
' | sponge ./target/bp/manifest.json | |
# generate rp terrain_texture.json | |
status_message process "Generating resource pack terrain texture definition" | |
jq -nc ' | |
{ | |
"resource_pack_name": "vanilla", | |
"texture_name": "atlas.terrain", | |
"padding": 8, | |
"num_mip_levels": 4, | |
"texture_data": { | |
"gmdl_atlas": { | |
"textures": "textures/blocks/geysercmd/gmdl_atlas" | |
} | |
} | |
} | |
' | sponge ./target/rp/textures/terrain_texture.json | |
status_message process "Generating resource pack disabling animation" | |
# generate our disabling animation | |
jq -nc ' | |
{ | |
"format_version": "1.8.0", | |
"animations": { | |
"animation.geysercmd.disable": { | |
"loop": true, | |
"override_previous_animation": true, | |
"bones": { | |
"geysercmd": { | |
"scale": 0 | |
} | |
} | |
} | |
} | |
} | |
' | sponge ./target/rp/animations/geysercmd/animation.geysercmd.disable.json | |
status_message completion "Initial pack setup complete\n" | |
jq -r '.[] | select(.parent != null) | [.path, .geyserID, .parent] | @tsv | gsub("\\t";",")' config.json | sponge pa.csv | |
_start=1 | |
_end="$(jq -r '(. | length) + ([.[] | select(.parent != null)] | length)' config.json)" | |
cur_pos=0 | |
function ProgressBar { | |
let _progress=(${1}*100/${2}*100)/100 | |
let _done=(${_progress}*6)/10 | |
let _left=60-$_done | |
_fill=$(printf "%${_done}s") | |
_empty=$(printf "%${_left}s") | |
printf "\r\e[37m█\e[m \e[37m${_fill// /█}\e[m\e[37m${_empty// /•}\e[m \e[37m█\e[m \e[33m${_progress}%\e[m\n" | |
} | |
# first, deal with parented models | |
while IFS=, read -r file gid parental | |
do | |
cur_pos=$((cur_pos+1)) | |
elements="$(jq -rc '.elements' ${file} | tee elements.temp)" | |
element_parent=${file} | |
textures="$(jq -rc '.textures' ${file} | tee textures.temp)" | |
display="$(jq -rc '.display' ${file} | tee display.temp)" | |
status_message process "Locating parental info for child model with GeyserID ${gid}" | |
# itterate through parented models until they all have geometry, display, and textures | |
until [[ ${elements} != null && ${textures} != null && ${display} != null ]] || [[ ${parental} = null ]] | |
do | |
if [[ ${elements} = null ]] | |
then | |
elements="$(jq -rc '.elements' ${parental} 2> /dev/null | tee elements.temp || echo && echo null)" | |
element_parent=${parental} | |
fi | |
if [[ ${textures} = null ]] | |
then | |
textures="$(jq -rc '.textures' ${parental} 2> /dev/null | tee textures.temp || echo && echo null)" | |
fi | |
if [[ ${display} = null ]] | |
then | |
display="$(jq -rc '.display' ${parental} 2> /dev/null | tee display.temp || echo && echo null)" | |
fi | |
parental="$(jq -rc 'def namespace: if contains(":") then sub("\\:(.+)"; "") else "minecraft" end; ("./assets/" + (.parent | namespace) + "/models/" + ((.parent? // empty) | sub("(.*?)\\:"; "")) + ".json") // "null"' ${parental} 2> /dev/null || echo && echo null)" | |
done | |
# if we can, generate a model now | |
if [[ ${elements} != null && ${textures} != null ]] | |
then | |
jq -n --slurpfile jelements elements.temp --slurpfile jtextures textures.temp --slurpfile jdisplay display.temp ' | |
{ | |
"textures": ($jtextures[]), | |
"elements": ($jelements[]) | |
} + (if $jdisplay then ({"display": ($jdisplay[])}) else {} end) | |
' | sponge ${file} | |
status_message completion "Located all parental info for Child ${gid}" | |
ProgressBar ${cur_pos} ${_end} | |
echo | |
# otherwise, remove it from our config | |
else | |
status_message plain "Suitable parent information was not availbile for ${gid}..." | |
status_message critical "Deleting ${gid} from config" | |
ProgressBar ${cur_pos} ${_end} | |
echo | |
jq --arg gid "${gid}" 'del(.[$gid])' config.json | sponge config.json | |
fi | |
done < pa.csv | |
# make sure we crop all mcmeta associated png files | |
status_message process "Cropping animated textures" | |
for i in $(find ./assets/**/textures -type f -name "*.mcmeta" | sed 's/\.mcmeta//'); do magick ${i} -background none -gravity North -extent "%[fx:h<w?h:w]x%[fx:h<w?h:w]" ${i}; done | |
status_message process "Compiling final model list" | |
# get our final model list from the config | |
model_list=( $(jq -r '.[].path' config.json) ) | |
# get our final texture list | |
# get a bash array of all texture files in our resource pack | |
status_message process "Generating an array of all model PNG files to crosscheck with our atlas" | |
jq -n '$ARGS.positional' --args $(find ./assets/**/textures -type f -name '*.png') | sponge textures1.temp | |
status_message process "Generating an array for the master texture atlas" | |
jq -s 'def namespace: if contains(":") then sub("\\:(.+)"; "") else "minecraft" end; [.[].textures[]] | unique | map("./assets/" + (. | namespace) + "/textures/" + (. | sub("(.*?)\\:"; "")) + ".png")' ${model_list[@]} | sponge textures2.temp | |
texture_list=( $(jq -s -r '((.[1] - (.[1] - .[0])) + ["./assets/minecraft/textures/0.png"]) | .[]' textures1.temp textures2.temp) ) | |
# generate our atlas from the final texture list | |
status_message process "Generating sprite sheet" | |
spritesheet-js -f json --fullpath ${texture_list[@]} > /dev/null 2>&1 | |
status_message completion "Sprite sheet successfully generated" | |
mv spritesheet.png ./target/rp/textures/blocks/geysercmd/gmdl_atlas.png | |
# begin conversion | |
jq -r '.[] | [.path, .geyserID] | @tsv | gsub("\\t";",")' config.json | sponge all.csv | |
while IFS=, read -r file gid | |
do | |
convert_model () { | |
local file=${1} | |
local gid=${2} | |
status_message process "Starting conversion of model with GeyserID ${gid}" | |
jq --slurpfile atlas spritesheet.json --arg binding "c.item_slot == 'head' ? 'head' : q.item_slot_to_bone_name(c.item_slot)" --arg model_name "${gid}" -c ' | |
.textures as $texture_list | | |
def namespace: if contains(":") then sub("\\:(.+)"; "") else "minecraft" end; | |
def totexture($input): ($texture_list[($input[1:])]? // ([$texture_list[]][0])); | |
def topath($input): ("./assets/" + ($input | namespace) + "/textures/" + ($input | sub("(.*?)\\:"; "")) + ".png"); | |
def texturedata($input): $atlas[] | .frames | (.[topath(totexture($input))] // ."./assets/minecraft/textures/0.png"); | |
def roundit: (.*10000 | round) / 10000; | |
def element_array: | |
.elements | map({ | |
"origin": [((-.to[0] + 8) | roundit), ((.from[1]) | roundit), ((.from[2] - 8) | roundit)], | |
"size": [((.to[0] - .from[0]) | roundit), ((.to[1] - .from[1]) | roundit), ((.to[2] - .from[2]) | roundit)], | |
"rotation": (if (.rotation.axis) == "x" then [(.rotation.angle | tonumber * -1), 0, 0] elif (.rotation.axis) == "y" then [0, (.rotation.angle | tonumber * -1), 0] elif (.rotation.axis) == "z" then [0, 0, (.rotation.angle | tonumber)] else null end), | |
"pivot": (if .rotation.origin then [((- .rotation.origin[0] + 8) | roundit), (.rotation.origin[1] | roundit), ((.rotation.origin[2] - 8) | roundit)] else null end), | |
"uv": ( | |
def uv_calc($input): | |
(if (.faces | .[$input]) then | |
(.faces | .[$input].texture) as $input_n | |
| ( (((((.faces | .[$input].uv[0]) * (texturedata($input_n) | .frame.w) * 0.0625) + (texturedata($input_n) | .frame.x)) * (16 / ($atlas[] | .meta.size.w))) ) ) as $fn0 | |
| ( (((((.faces | .[$input].uv[1]) * (texturedata($input_n) | .frame.h) * 0.0625) + (texturedata($input_n) | .frame.y)) * (16 / ($atlas[] | .meta.size.h))) ) ) as $fn1 | |
| ( (((((.faces | .[$input].uv[2]) * (texturedata($input_n) | .frame.w) * 0.0625) + (texturedata($input_n) | .frame.x)) * (16 / ($atlas[] | .meta.size.w))) ) ) as $fn2 | |
| ( (((((.faces | .[$input].uv[3]) * (texturedata($input_n) | .frame.h) * 0.0625) + (texturedata($input_n) | .frame.y)) * (16 / ($atlas[] | .meta.size.h))) ) ) as $fn3 | |
| (($fn2 - $fn0) as $num | [([-1, $num] | max), 1] | min) as $x_sign | |
| (($fn3 - $fn1) as $num | [([-1, $num] | max), 1] | min) as $y_sign | | |
{ | |
"uv": [(($fn0 + (0.016 * $x_sign)) | roundit), (($fn1 + (0.016 * $y_sign)) | roundit)], | |
"uv_size": [((($fn2 - $fn0) - (0.016 * $x_sign)) | roundit), ((($fn3 - $fn1) - (0.016 * $y_sign)) | roundit)] | |
} else null end); | |
{ | |
"north": uv_calc("north"), | |
"south": uv_calc("south"), | |
"east": uv_calc("east"), | |
"west": uv_calc("west"), | |
"up": uv_calc("up"), | |
"down": uv_calc("down") | |
}) | |
}) | walk( if type == "object" then with_entries(select(.value != null)) else . end) | |
; | |
def pivot_groups: | |
(element_array) as $element_array | | |
[[.elements[].rotation] | unique | .[] | select (.!=null)] | |
| map(( | |
[((- .origin[0] + 8) | roundit), (.origin[1] | roundit), ((.origin[2] - 8) | roundit)] as $i_piv | | |
(if (.axis) == "x" then [(.angle | tonumber * -1), 0, 0] elif (.axis) == "y" then [0, (.angle | tonumber * -1), 0] else [0, 0, (.angle | tonumber)] end) as $i_rot | | |
{ | |
"parent": "geysercmd_z", | |
"pivot": ($i_piv), | |
"rotation": ($i_rot), | |
"mirror": true, | |
"cubes": [($element_array | .[] | select(.rotation == $i_rot and .pivot == $i_piv))] | |
})) | |
; | |
{ | |
"format_version": "1.16.0", | |
"minecraft:geometry": [{ | |
"description": { | |
"identifier": ("geometry.geysercmd." + ($model_name)), | |
"texture_width": 16, | |
"texture_height": 16, | |
"visible_bounds_width": 4, | |
"visible_bounds_height": 4.5, | |
"visible_bounds_offset": [0, 0.75, 0] | |
}, | |
"bones": ([{ | |
"name": "geysercmd", | |
"binding": $binding, | |
"pivot": [0, 8, 0] | |
}, { | |
"name": "geysercmd_x", | |
"parent": "geysercmd", | |
"pivot": [0, 8, 0] | |
}, { | |
"name": "geysercmd_y", | |
"parent": "geysercmd_x", | |
"pivot": [0, 8, 0] | |
}, { | |
"name": "geysercmd_z", | |
"parent": "geysercmd_y", | |
"pivot": [0, 8, 0], | |
"cubes": [(element_array | .[] | select(.rotation == null))] | |
}] + (pivot_groups | map(del(.cubes[].rotation)) | to_entries | map( (.value.name = "rot_\(1+.key)" ) | .value))) | |
}] | |
} | |
' ${file} | sponge ./target/rp/models/blocks/geysercmd/${gid}.json | |
# generate our rp animations via display settings | |
jq -c --arg model_name "${gid}" ' | |
{ | |
"format_version": "1.8.0", | |
"animations": { | |
("animation.geysercmd." + ($model_name) + ".thirdperson_main_hand"): { | |
"loop": true, | |
"bones": { | |
"geysercmd_x": (if .display.thirdperson_righthand then { | |
"rotation": (if .display.thirdperson_righthand.rotation then [(- .display.thirdperson_righthand.rotation[0]), 0, 0] else null end), | |
"position": (if .display.thirdperson_righthand.translation then [(- .display.thirdperson_righthand.translation[0]), (.display.thirdperson_righthand.translation[1]), (.display.thirdperson_righthand.translation[2])] else null end), | |
"scale": (if .display.thirdperson_righthand.scale then [(.display.thirdperson_righthand.scale[0]), (.display.thirdperson_righthand.scale[1]), (.display.thirdperson_righthand.scale[2])] else null end) | |
} else null end), | |
"geysercmd_y": (if .display.thirdperson_righthand.rotation then { | |
"rotation": (if .display.thirdperson_righthand.rotation then [0, (- .display.thirdperson_righthand.rotation[1]), 0] else null end) | |
} else null end), | |
"geysercmd_z": (if .display.thirdperson_righthand.rotation then { | |
"rotation": [0, 0, (.display.thirdperson_righthand.rotation[2])] | |
} else null end), | |
"geysercmd": { | |
"rotation": [90, 0, 0], | |
"position": [0, 13, -3] | |
} | |
} | |
}, | |
("animation.geysercmd." + ($model_name) + ".thirdperson_off_hand"): { | |
"loop": true, | |
"bones": { | |
"geysercmd_x": (if .display.thirdperson_lefthand then { | |
"rotation": (if .display.thirdperson_lefthand.rotation then [(- .display.thirdperson_lefthand.rotation[0]), 0, 0] else null end), | |
"position": (if .display.thirdperson_lefthand.translation then [(- .display.thirdperson_lefthand.translation[0]), (.display.thirdperson_lefthand.translation[1]), (.display.thirdperson_lefthand.translation[2])] else null end), | |
"scale": (if .display.thirdperson_lefthand.scale then [(.display.thirdperson_lefthand.scale[0]), (.display.thirdperson_lefthand.scale[1]), (.display.thirdperson_lefthand.scale[2])] else null end) | |
} else null end), | |
"geysercmd_y": (if .display.thirdperson_lefthand.rotation then { | |
"rotation": (if .display.thirdperson_lefthand.rotation then [0, (- .display.thirdperson_lefthand.rotation[1]), 0] else null end) | |
} else null end), | |
"geysercmd_z": (if .display.thirdperson_lefthand.rotation then { | |
"rotation": [0, 0, (.display.thirdperson_lefthand.rotation[2])] | |
} else null end), | |
"geysercmd": { | |
"rotation": [90, 0, 0], | |
"position": [0, 13, -3] | |
} | |
} | |
}, | |
("animation.geysercmd." + ($model_name) + ".head"): { | |
"loop": true, | |
"bones": { | |
"geysercmd_x": { | |
"rotation": (if .display.head.rotation then [(- .display.head.rotation[0]), 0, 0] else null end), | |
"position": (if .display.head.translation then [(- .display.head.translation[0] * 0.625), (.display.head.translation[1] * 0.625), (.display.head.translation[2] * 0.625)] else null end), | |
"scale": (if .display.head.scale then (.display.head.scale | map(. * 0.625)) else 0.625 end) | |
}, | |
"geysercmd_y": (if .display.head.rotation then { | |
"rotation": [0, (- .display.head.rotation[1]), 0] | |
} else null end), | |
"geysercmd_z": (if .display.head.rotation then { | |
"rotation": [0, 0, (.display.head.rotation[2])] | |
} else null end), | |
"geysercmd": { | |
"position": [0, 19.5, 0] | |
} | |
} | |
}, | |
("animation.geysercmd." + ($model_name) + ".firstperson_main_hand"): { | |
"loop": true, | |
"bones": { | |
"geysercmd": { | |
"rotation": [90, 60, -40], | |
"position": [4, 10, 4], | |
"scale": 1.5 | |
}, | |
"geysercmd_x": { | |
"position": (if .display.firstperson_righthand.translation then [(- .display.firstperson_righthand.translation[0]), (.display.firstperson_righthand.translation[1]), (- .display.firstperson_righthand.translation[2])] else null end), | |
"rotation": (if .display.firstperson_righthand.rotation then [(- .display.firstperson_righthand.rotation[0]), 0, 0] else [0.1, 0.1, 0.1] end), | |
"scale": (if .display.firstperson_righthand.scale then (.display.firstperson_righthand.scale) else null end) | |
}, | |
"geysercmd_y": (if .display.firstperson_righthand.rotation then { | |
"rotation": [0, (- .display.firstperson_righthand.rotation[1]), 0] | |
} else null end), | |
"geysercmd_z": (if .display.firstperson_righthand.rotation then { | |
"rotation": [0, 0, (.display.firstperson_righthand.rotation[2])] | |
} else null end) | |
} | |
}, | |
("animation.geysercmd." + ($model_name) + ".firstperson_off_hand"): { | |
"loop": true, | |
"bones": { | |
"geysercmd": { | |
"rotation": [90, 60, -40], | |
"position": [4, 10, 4], | |
"scale": 1.5 | |
}, | |
"geysercmd_x": { | |
"position": (if .display.firstperson_lefthand.translation then [(.display.firstperson_lefthand.translation[0]), (.display.firstperson_lefthand.translation[1]), (- .display.firstperson_lefthand.translation[2])] else null end), | |
"rotation": (if .display.firstperson_lefthand.rotation then [(- .display.firstperson_lefthand.rotation[0]), 0, 0] else [0.1, 0.1, 0.1] end), | |
"scale": (if .display.firstperson_lefthand.scale then (.display.firstperson_lefthand.scale) else null end) | |
}, | |
"geysercmd_y": (if .display.firstperson_lefthand.rotation then { | |
"rotation": [0, (- .display.firstperson_lefthand.rotation[1]), 0] | |
} else null end), | |
"geysercmd_z": (if .display.firstperson_lefthand.rotation then { | |
"rotation": [0, 0, (.display.firstperson_lefthand.rotation[2])] | |
} else null end) | |
} | |
} | |
} | |
} | walk( if type == "object" then with_entries(select(.value != null)) else . end) | |
' ${file} | sponge ./target/rp/animations/geysercmd/animation.${gid}.json | |
# generate our rp attachable definition | |
jq -c -n --arg attachable_material "${attachable_material}" --arg v_main "v.main_hand = c.item_slot == 'main_hand';" --arg v_off "v.off_hand = c.item_slot == 'off_hand';" --arg v_head "v.head = c.item_slot == 'head';" --arg model_name "${gid}" --arg texture_name "${texture_id}" ' | |
{ | |
"format_version": "1.10.0", | |
"minecraft:attachable": { | |
"description": { | |
"identifier": ("geysercmd:" + $model_name), | |
"materials": { | |
"default": $attachable_material, | |
"enchanted": $attachable_material | |
}, | |
"textures": { | |
"default": "textures/blocks/geysercmd/gmdl_atlas", | |
"enchanted": "textures/misc/enchanted_item_glint" | |
}, | |
"geometry": { | |
"default": ("geometry.geysercmd." + $model_name) | |
}, | |
"scripts": { | |
"pre_animation": [$v_main, $v_off, $v_head], | |
"animate": [ | |
{"thirdperson_main_hand": "v.main_hand && !c.is_first_person"}, | |
{"thirdperson_off_hand": "v.off_hand && !c.is_first_person"}, | |
{"thirdperson_head": "v.head && !c.is_first_person"}, | |
{"firstperson_main_hand": "v.main_hand && c.is_first_person"}, | |
{"firstperson_off_hand": "v.off_hand && c.is_first_person"}, | |
{"firstperson_head": "c.is_first_person && v.head"} | |
] | |
}, | |
"animations": { | |
"thirdperson_main_hand": ("animation.geysercmd." + $model_name + ".thirdperson_main_hand"), | |
"thirdperson_off_hand": ("animation.geysercmd." + $model_name + ".thirdperson_off_hand"), | |
"thirdperson_head": ("animation.geysercmd." + $model_name + ".head"), | |
"firstperson_main_hand": ("animation.geysercmd." + $model_name + ".firstperson_main_hand"), | |
"firstperson_off_hand": ("animation.geysercmd." + $model_name + ".firstperson_off_hand"), | |
"firstperson_head": "animation.geysercmd.disable" | |
}, | |
"render_controllers": [ "controller.render.item_default" ] | |
} | |
} | |
} | |
' | sponge ./target/rp/attachables/geysercmd/${gid}.attachable.json | |
# generate our bp block definition | |
jq -c -n --arg block_material "${block_material}" --arg geyser_id "${gid}" ' | |
{ | |
"format_version": "1.16.200", | |
"minecraft:block": { | |
"description": { | |
"identifier": ("geysercmd:" + $geyser_id) | |
}, | |
"components": { | |
"minecraft:material_instances": { | |
"*": { | |
"texture": "gmdl_atlas", | |
"render_method": $block_material, | |
"face_dimming": false, | |
"ambient_occlusion": false | |
} | |
}, | |
"tag:geysercmd:example_block": {}, | |
"minecraft:geometry": ("geometry.geysercmd." + $geyser_id), | |
"minecraft:placement_filter": { | |
"conditions": [ | |
{ | |
"allowed_faces": [ | |
], | |
"block_filter": [ | |
] | |
} | |
] | |
} | |
} | |
} | |
} | |
' | sponge ./target/bp/blocks/geysercmd/${gid}.json | |
local tot_pos=$((cur_pos + $(ls ./target/bp/blocks/geysercmd/*.json | wc -l))) | |
status_message completion "${gid} converted\n$(ProgressBar ${tot_pos} ${_end})" | |
echo | |
} | |
convert_model ${file} ${gid} & | |
done < all.csv | |
wait # wait for all the jobs to finish | |
# write lang file US | |
status_message process "Writing en_US and en_GB lang files" | |
mkdir ./target/rp/texts | |
jq -r ' | |
def format: (.[0:1] | ascii_upcase ) + (.[1:] | gsub( "_(?<a>[a-z])"; (" " + .a) | ascii_upcase)); | |
to_entries[]|"\("tile.geysercmd:" + .key + ".name")=\(.value.item | format)" | |
' config.json | sponge ./target/rp/texts/en_US.lang | |
# copy US lang to GB | |
cp ./target/rp/texts/en_US.lang ./target/rp/texts/en_GB.lang | |
# write supported languages file | |
jq -n '["en_US","en_GB"]' | sponge ./target/rp/texts/languages.json | |
status_message completion "en_US and en_GB lang files written\n" | |
# apply image compression if we can | |
#if command -v pngquant >/dev/null 2>&1 ; then | |
# status_message completion "Optional dependency pngquant detected" | |
# status_message process "Attempting image compression" | |
# pngquant -f --skip-if-larger --ext .png --strip ./target/rp/textures/blocks/geysercmd/*.png | |
# status_message completion "Image compression complete" | |
# echo | |
#fi | |
# attempt to merge with existing pack if input was provided | |
if test -f ${merge_input}; then | |
mkdir inputbedrockpack | |
status_message process "Decompressing input bedrock pack" | |
unzip -q ${merge_input} -d ./inputbedrockpack | |
status_message process "Merging input bedrock pack with generated bedrock assets" | |
cp -n -r "./inputbedrockpack"/* './target/rp/' | |
if test -f ./inputbedrockpack/textures/terrain_texture.json; then | |
status_message process "Merging terrain texture files" | |
jq -s ' | |
{"resource_pack_name": "vanilla", | |
"texture_name": "atlas.terrain", | |
"padding": 8, "num_mip_levels": 4, | |
"texture_data": (.[1].texture_data + .[0].texture_data)} | |
' ./target/rp/textures/terrain_texture.json ./inputbedrockpack/textures/terrain_texture.json | sponge ./target/rp/textures/terrain_texture.json | |
fi | |
if test -f ./inputbedrockpack/texts/languages.json; then | |
status_message process "Merging languages file" | |
jq -s '.[0] + .[1] | unique' | sponge ./target/rp/texts/languages.json | |
fi | |
if test -f ./inputbedrockpack/texts/en_US.lang; then | |
status_message process "Merging en_US lang file" | |
cat ./inputbedrockpack/texts/en_US.lang >> ./target/rp/texts/en_US.lang | |
fi | |
if test -f ./inputbedrockpack/texts/en_GB.lang; then | |
status_message process "Merging en_GB lang file" | |
cat ./inputbedrockpack/texts/en_GB.lang >> ./target/rp/texts/en_GB.lang | |
fi | |
status_message critical "Deleting input bedrock pack scratch direcotry" | |
rm -rf inputbedrockpack | |
status_message completion "Input bedrock pack merged with generated assets\n" | |
fi | |
# cleanup | |
status_message critical "Deleting scratch files" | |
rm -rf assets && rm -f pack.mcmeta && rm -f pack.png && rm -f parents.json && rm -f all.csv && rm -f pa.csv && rm -f README.md && rm -f README.txt && rm -f *.temp && rm -f spritesheet.json | |
status_message critical "Deleting unused entries from config" | |
jq 'map_values(del(.path, .element_parent, .parent, .geyserID))' config.json | sponge config.json | |
status_message process "Moving config to target directory" | |
mv config.json ./target/config.json | |
echo | |
status_message process "Compressing output packs" | |
mkdir ./target/packaged | |
cd ./target/rp > /dev/null && zip -rq8 geyser_rp.mcpack . -x "*/.*" && cd - > /dev/null && mv ./target/rp/geyser_rp.mcpack ./target/packaged/geyser_rp.mcpack | |
cd ./target/bp > /dev/null && zip -rq8 geyser_bp.mcpack . -x "*/.*" && cd - > /dev/null && mv ./target/bp/geyser_bp.mcpack ./target/packaged/geyser_bp.mcpack | |
cd ./target/packaged > /dev/null && zip -rq8 geyser.mcaddon . -i "*.mcpack" && cd - > /dev/null | |
mkdir ./target/unpackaged | |
mv ./target/rp ./target/unpackaged/rp && mv ./target/bp ./target/unpackaged/bp | |
echo | |
printf "\e[32m[+]\e[m \e[1m\e[37mConversion Process Complete\e[m\n\n\e[37mExiting...\e[m\n\n" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Usage
To run, simply:
To run without settings prompts and use the defaults:
About
The script has been updated to handle parent models. It will generate a single sprite sheet for all textures.
Your script and resource pack zip file must be in the same directory. Ensure that this zip file is properly setup. It should not have a root directory. Your resource pack must also be formatted correctly, to vanilla specifications. By default, this script will download the default assets in order to generate texture atlases in cases in which you have utilized those. If you wish to use different default assets, you may specify this at the beginning. However, you must ensure that the specified fallback pack has sufficient assets to supply any texture request from any predicate model or model in a predicate model's parental tree. As long as you provide valid JSON, the script should output something you can use.
You may also specify an input bedrock pack to merge with the output assets produced by the converter. This should be in the same directory as the script and your input Java pack. Like the Java pack, ensure that it is compressed with no root folder. When prompted, type the file name of the input file, such as
example_rp.mcpack
. Merging of behavior packs is not currently supported.The packs generated by this script can currently only be used in Bedrock Edition 1.16.210.59 and above. Please be sure to enable the experimental setting "Holiday Creator Features" on world generation. You'll probably also want to be in creative mode as that will be the only way to give yourself the blocks generated by the script. Here are the commands to obtain one of the items or place them in your head slot:
The #### term corresponds to the numerical ID assigned to the model. These are written to config.json, which can be found in the target directory, along with packaged and non-packaged versions of the output assets. This will specify the corresponding item and nbt for each input predicate.
Also note that this script requires:
It will exit if you fail to meet these dependencies. While the ultimate intention here is to import these via Geyser, the script currently generates a behavior pack so that you may view your converted models in Bedrock Edition. For progress on this, see Geyser Issue 210.
Should you have any complaints about getting this to run because you "only use windows", do note that I was able to run this perfectly fine on a Mac running Parallels to run Windows to run WSL to run Ubuntu 20. Therefore, I am sure it is perfectly possible for you to get this working, regardless of your OS. I will gladly take such criticism only if you are willing to help develop this into a proper cross platform program.
Dependency Installation
A note about ImageMagick
It seems many of the package repositories still install ImageMagick 6. If this occurs and the script fails for this reason, consider using IMEI to build ImageMagick 7 from source.
Debian, Ubuntu, & Mint
MacOS
RHEL, Fedora, & Centos
Arch Linux
Windows
Impossible; consider WSL.
Special Notes for WSL
In general, packages on WSL seem to be a little wonky, and sometimes jq-1.5 will be installed, which will not work. To manually install jq-1.6 on WSL, at least for Ubuntu: