Skip to content

Instantly share code, notes, and snippets.

@Kas-tle
Last active December 3, 2023 11:04
Show Gist options
  • Save Kas-tle/a96794a0fe52075329dcb488e41b22b1 to your computer and use it in GitHub Desktop.
Save Kas-tle/a96794a0fe52075329dcb488e41b22b1 to your computer and use it in GitHub Desktop.
Java to Bedrock 3D Model Converter
#!/usr/bin/env bash
: ${1?'Please specify an input resource pack in the same directory as the script (e.g. ./converter.sh MyResourcePack.zip)'}
# ensure input pack exists
if ! test -f "${1}"; then
echo "Input resource pack ${1} is not in this directory"
echo "Please ensure you have entered the filename correctly"
exit 1
else
printf "\e[33m[•]\e[m \e[37mInput file ${1} detected\e[m\n"
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
if command jq --version 2>/dev/null | grep -q "1.6"; then
printf "\e[32m[+]\e[m \e[37mDependency jq satisfied\e[m\n"
echo
else
echo "Dependency jq-1.6 is not satisfied"
echo "You must install jq-1.6 before proceeding"
echo "See https://stedolan.github.io/jq/download/"
echo "Exiting script..."
exit 1
fi
if command -v sponge >/dev/null 2>&1 ; then
printf "\e[32m[+]\e[m \e[37mDependency sponge satisfied\e[m\n"
echo
else
echo "Dependency sponge is not satisfied"
echo "You must install sponge before proceeding"
echo "See https://joeyh.name/code/moreutils/"
echo "Exiting script..."
exit 1
fi
if command -v convert >/dev/null 2>&1 ; then
printf "\e[32m[+]\e[m \e[37mDependency imagemagick satisfied\e[m\n"
echo
else
echo "Dependency imagemagick is not satisfied"
echo "You must install imagemagick before proceeding"
echo "See https://imagemagick.org/script/download.php"
echo "Exiting script..."
exit 1
fi
printf "\e[32m[+]\e[m \e[37mAll dependencies have been satisfied\e[m\n"
echo
# initial configuration
if [[ ${2} != default ]]
then
printf "\e[36mThis script will now ask some configuration question. Default values are yellow. Simply press enter to use the defaults.\e[m\n"
echo
printf "\e[1m\e[37mIs there an existing bedrock pack in this directory with which you would like the output merged? (e.g. input.mcpack)\e[m \e[33m[null]\e[m\n"
echo
read -p $'\e[37mInput pack to merge:\e[0m ' merge_input
echo
printf "\e[1m\e[37mWhat is the max width dimension we should allow for an input texture without downscaling?\e[m \e[33m[128]\e[m\n"
echo
read -p $'\e[37mMax width dimension:\e[0m ' maximum_width_dimension
echo
printf "\e[1m\e[37mWhat material should we use for the attachables?\e[m \e[33m[entity_alphatest]\e[m\n"
printf "\e[3m\e[37mFor more info, see:\e[m \e[36m[https://wiki.bedrock.dev/documentation/materials]\e[m\n"
echo
read -p $'\e[37mAttachable material:\e[0m ' attachable_material
echo
printf "\e[1m\e[37mWhat material should we use for the blocks? (e.g. opaque)\e[m \e[33m[alpha_test]\e[m\n"
printf "\e[3m\e[37mFor more info, see:\e[m \e[36m[https://wiki.bedrock.dev/documentation/block-model-materials]\e[m\n"
echo
read -p $'\e[37mBlock material:\e[0m ' block_material
echo
printf "\e[1m\e[37mFrom what URL should we download the fallback resource pack? (must be a direct link)\e[m \e[33m[null]\e[m\n"
printf "\e[3m\e[37mIf left blank, we will use our own sources to download the default assets.\e[m\n"
printf "\e[3m\e[37mEnsure any specified pack has all required predicate textures.\e[m\n"
printf "\e[3m\e[37mIf your input pack already contains all required predicate textures, use 'none' to skip fallback asset download.\e[m\n"
echo
read -p $'\e[37mFallback pack URL:\e[0m ' fallback_pack
echo
echo
echo
fi
printf "\e[37mGenerating Bedrock 3D resource pack with settings:\e[m\n"
printf "\e[37mInput pack to merge:\e[m \e[36m${merge_input:=null}\e[m\n"
printf "\e[37mMax width dimension:\e[m \e[36m${maximum_width_dimension:=128}\e[m\n"
printf "\e[37mAttachable material:\e[m \e[36m${attachable_material:=entity_alphatest}\e[m\n"
printf "\e[37mBlock material:\e[m \e[36m${block_material:=alpha_test}\e[m\n"
printf "\e[37mFallback pack URL:\e[m \e[36m${fallback_pack:=null}\e[m\n"
echo
# decompress our input pack
printf "\e[33m[•]\e[m \e[37mDecompressing input pack\e[m\n"
echo
unzip -q ${1}
printf "\e[32m[+]\e[m \e[37mInput pack decompressed\e[m\n"
echo
# get the current default textures and merge them with our rp
if [[ ${fallback_pack} != none ]]
then
printf "\e[33m[•]\e[m \e[37mNow downloading the fallback resource pack:\e[m\n"
echo
fi
if [[ ${fallback_pack} = null ]]
then
printf "\e[3m\e[37m"
wget -nv --show-progress -O default_assets.zip https://api.github.com/repos/InventivetalentDev/minecraft-assets/zipball/1.16.5
printf "\e[m"
echo
printf "\e[32m[+]\e[m \e[37mFallback resources downloaded\e[m\n"
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}"
printf "\e[m"
echo
printf "\e[32m[+]\e[m \e[37mFallback resources downloaded\e[m\n"
root_folder=""
fi
if [[ ${fallback_pack} != none ]]
then
mkdir ./defaultassetholding
unzip -q -d ./defaultassetholding default_assets.zip "${root_folder}assets/minecraft/textures/**/*"
printf "\e[32m[+]\e[m \e[37mFallback resources decompressed\e[m\n"
cp -n -r "./defaultassetholding/${root_folder}assets/minecraft/textures"/* './assets/minecraft/textures/'
printf "\e[32m[+]\e[m \e[37mFallback resources merged with target pack\e[m\n"
rm -rf defaultassetholding
rm -f default_assets.zip
printf "\e[31m[X]\e[m \e[37mExtraneous fallback resources deleted\e[m\n"
echo
fi
# generate a fallback texture
convert -size 16x16 xc:\#FFFFFF ./assets/minecraft/textures/fallbacktexture.png
# setup our initial config
printf "\e[33m[•]\e[m \e[37mIterating through all vanilla associated model JSONs to generate initial predicate config\e[m\n"
printf "\e[3m\e[37mOn a large pack, this may take some time...\e[m\n"
echo
# check if we have block and item folders
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))
| to_entries | map( ((.value.geometry = ("geometry.geysercmd." + "gmdl_\(1+.key)")) | .value))
| INDEX(.geyserID)
' ${confarg1} ${confarg2} | sponge config.json
printf "\e[32m[+]\e[m \e[37mInitial predicate config generated\e[m\n"
# get a bash array of all model json files in our resource pack
printf "\e[33m[•]\e[m \e[37mGenerating an array of all model JSON files to crosscheck with our predicate config\e[m\n"
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
printf "\e[31m[X]\e[m \e[37mRemoving config entries that do not have an associated JSON file in the pack\e[m\n"
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
printf "\e[33m[•]\e[m \e[37mCreating a bash array for remaing models in our predicate config\e[m\n"
model_array=($(jq -r '.[].path' config.json))
# find initial parental information
printf "\e[33m[•]\e[m \e[37mDoing an initial sweep for level 1 parentals\e[m\n"
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
printf "\e[31m[X]\e[m \e[37mRemoving config entries with non-supported parentals\e[m\n"
echo
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
printf "\e[33m[•]\e[m \e[37mGenerating initial directory strucutre for our bedrock packs\e[m\n"
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
printf "\e[33m[•]\e[m \e[37mGenerating resource pack manifest\e[m\n"
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
printf "\e[33m[•]\e[m \e[37mGenerating behavior pack manifest\e[m\n"
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
printf "\e[33m[•]\e[m \e[37mGenerating resource pack terrain texture definition\e[m\n"
jq -nc '
{
"resource_pack_name": "vanilla",
"texture_name": "atlas.terrain",
"padding": 8,
"num_mip_levels": 4,
"texture_data": {}
}
' | sponge ./target/rp/textures/terrain_texture.json
printf "\e[33m[•]\e[m \e[37mGenerating resource pack disabling animation\e[m\n"
# 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
printf "\e[32m[+]\e[m \e[37mInitial pack setup complete\e[m\n"
echo
jq -r '.[] | select(.parent != null) | [.path, .geyserID, .parent] | @tsv | gsub("\\t";",")' config.json | sponge pa.csv
jq -r '.[] | select(.parent == null) | [.path, .geyserID] | @tsv | gsub("\\t";",")' config.json | sponge np.csv
_start=1
_end="$(jq -r '. | 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"
echo
}
# deal with non-parental models [select(.parent == null)]
while IFS=, read -r file gid
do
cur_pos=$((cur_pos+1))
printf "\e[33m[•]\e[m \e[37mStarting conversion of primary model with GeyserID ${gid}\e[m\n"
# get texture array
texture_array=($(jq -r 'def namespace: if contains(":") then sub("\\:(.+)"; "") else "minecraft" end; .textures | to_entries | sort_by(.key) | map({(.key): .value}) | add | map("./assets/" + (. | namespace) + "/textures/" + (. | sub("(.*?)\\:"; "")) + ".png") | .[]' ${file}))
# crop any mcmeta associated files on the first frame and make sure we have a texture
tex_array_counter=0
for i in "${texture_array[@]}"
do
if test -f "${i}.mcmeta"; then
magick ${i} -background none -gravity North -extent "%[fx:h<w?h:w]x%[fx:h<w?h:w]" ${i}
printf "\e[31m[!]\e[m \e[37mCropped .textures[${tex_array_counter}] for use in ${gid} becuase mcmeta was detected\e[m\n"
rm "${i}.mcmeta"
fi
if ! test -f "${i}"; then
texture_array[${tex_array_counter}]="./assets/minecraft/textures/fallbacktexture.png"
printf "\e[31m[!]\e[m \e[37mFallback texture was used for ${gid}.textures[${tex_array_counter}]\e[m\n"
fi
tex_array_counter=$((tex_array_counter+1))
done
tile_dim=($(jq -r '.textures | length | sqrt | ceil' ${file}))
# find widest image & cap texture_width @ 128
texture_width=($(identify -format "%w\n" ${texture_array[@]} | sort -n -r -k 1 | head -n 1))
texture_width=$(($texture_width <= ${maximum_width_dimension} ? $texture_width : ${maximum_width_dimension}))
# generate stitched texture with imagemagick (cap size @512) (perhaps we can filter to used textures only)
montage ${texture_array[@]} -background transparent -geometry x${texture_width}+0+0 -tile ${tile_dim}x${tile_dim} -interpolate Integer -filter point ./target/rp/textures/blocks/geysercmd/${gid}.png
# ensure we do not already have this texture, and reuse in the case that we do
texture_id=${gid}
texture_hash=($(md5 -q "./target/rp/textures/blocks/geysercmd/${gid}.png" 2>/dev/null))
match_count=($(md5 -r ./target/rp/textures/blocks/geysercmd/*.png 2>/dev/null | grep "${texture_hash}" | wc -l))
if [ ${match_count} -gt 1 ]
then
# remove our newly generated duplicate texture and change our texture_id to the match
rm "./target/rp/textures/blocks/geysercmd/${gid}.png"
texture_id=($(md5 -r ./target/rp/textures/blocks/geysercmd/*.png | grep "${texture_hash}" | cut -d "." -f 2 | cut -c38-))
printf "\e[33m[•]\e[m \e[37mReusing texture from ${texture_id} for ${gid}\e[m\n"
fi
# append our texture information to rp terrain_texture.json
jq -c --arg texture_id "${texture_id}" --arg geyser_id "${gid}" '
.texture_data += {
($geyser_id): {
"textures": ("textures/blocks/geysercmd/" + $texture_id)
}
}
' ./target/rp/textures/terrain_texture.json | sponge ./target/rp/textures/terrain_texture.json
# convert our rp geometry file from java to bedrock
jq --arg binding "c.item_slot == 'head' ? 'head' : q.item_slot_to_bone_name(c.item_slot)" --arg model_name "${gid}" -c '
def element_array:
(.textures | to_entries | sort_by(.key) | map({(.key): .value}) | add | keys_unsorted) as $texture_array
| ($texture_array | length) as $frames
| (($frames | sqrt) | ceil) as $sides
| (.texture_size[1] // 16) as $t1
| .elements | map({
"origin": [(-.to[0] + 8), (.from[1]), (.from[2] - 8)],
"size": [.to[0] - .from[0], .to[1] - .from[1], .to[2] - .from[2]],
"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), .rotation.origin[1], (.rotation.origin[2] - 8)] else null end),
"uv": (
def uv_calc($input):
(if (.faces | .[$input]) then
(.faces | .[$input].texture[1:] as $input_n | $texture_array | (index($input_n) // index("particle"))) as $pos_n
| ((.faces | .[$input].uv[0] / $sides) + ((fmod($pos_n; $sides)) * (16 / $sides))) as $fn0
| ((.faces | .[$input].uv[1] / $sides) + ((($pos_n / $sides) | floor) * (16 / $sides))) as $fn1
| ((.faces | .[$input].uv[2] / $sides) + ((fmod($pos_n; $sides)) * (16 / $sides))) as $fn2
| ((.faces | .[$input].uv[3] / $sides) + ((($pos_n / $sides) | floor) * (16 / $sides))) as $fn3 |
{
"uv": [($fn0), ($fn1)],
"uv_size": [($fn2 - $fn0), ($fn3 - $fn1)]
} 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), .origin[1], (.origin[2] - 8)] 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/" + $texture_name),
"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": $geyser_id,
"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
printf "\e[32m[+]\e[m \e[37m${gid} converted\e[m\n"
ProgressBar ${cur_pos} ${_end}
echo
done < np.csv
printf "\e[32m[+]\e[m \e[37mFinished conversion of primary models\e[m\n"
echo
printf "\e[33m[•]\e[m \e[37mStarting conversion of child models\e[m\n"
echo
# deal with parental models [select(.parent != null)]
while IFS=, read -r file gid parental
do
cur_pos=$((cur_pos+1))
elements="$(jq -rc '.elements' ${file})"
element_parent=${file}
textures="$(jq -rc '.textures' ${file})"
display="$(jq -rc '.display' ${file})"
printf "\e[33m[•]\e[m \e[37mStarting conversion attempt for child model with GeyserID ${gid}\e[m\n"
until [[ ${elements} != null && ${textures} != null && ${display} != null ]] || [[ ${parental} = null ]]
do
if [[ ${elements} = null ]]
then
elements="$(jq -rc '.elements' ${parental})"
element_parent=${parental}
fi
if [[ ${textures} = null ]]
then
textures="$(jq -rc '.textures' ${parental})"
fi
if [[ ${display} = null ]]
then
display="$(jq -rc '.display' ${parental})"
fi
parental="$(jq -rc 'def namespace: if contains(":") then sub("\\:(.+)"; "") else "minecraft" end; ("./assets/" + (.parent | namespace) + "/models/" + ((.parent? // empty) | sub("(.*?)\\:"; "")) + ".json") // "null"' ${parental})"
done
if [[ ${elements} != null && ${textures} != null && ${display} != null ]]
then
matching_element="$(jq -rc --arg element_parent "${element_parent}" '(.[] | select(.element_parent == $element_parent or .path == $element_parent) | .geyserID) // "null"' config.json)"
if [[ ${matching_element} = null ]]
then
jq -n -c --arg binding "c.item_slot == 'head' ? 'head' : q.item_slot_to_bone_name(c.item_slot)" --arg model_name "${gid}" --argjson jelements "${elements}" --argjson jtextures "${textures}" '{"textures": $jtextures, "elements": $jelements} |
def element_array:
(.textures | to_entries | sort_by(.key) | map({(.key): .value}) | add | keys_unsorted) as $texture_array
| ($texture_array | length) as $frames
| (($frames | sqrt) | ceil) as $sides
| (.texture_size[1] // 16) as $t1
| .elements | map({
"origin": [(-.to[0] + 8), (.from[1]), (.from[2] - 8)],
"size": [.to[0] - .from[0], .to[1] - .from[1], .to[2] - .from[2]],
"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), .rotation.origin[1], (.rotation.origin[2] - 8)] else null end),
"uv": (
def uv_calc($input):
(if (.faces | .[$input]) then
(.faces | .[$input].texture[1:] as $input_n | $texture_array | (index($input_n) // index("particle"))) as $pos_n
| ((.faces | .[$input].uv[0] / $sides) + ((fmod($pos_n; $sides)) * (16 / $sides))) as $fn0
| ((.faces | .[$input].uv[1] / $sides) + ((($pos_n / $sides) | floor) * (16 / $sides))) as $fn1
| ((.faces | .[$input].uv[2] / $sides) + ((fmod($pos_n; $sides)) * (16 / $sides))) as $fn2
| ((.faces | .[$input].uv[3] / $sides) + ((($pos_n / $sides) | floor) * (16 / $sides))) as $fn3 |
{
"uv": [($fn0), ($fn1)],
"uv_size": [($fn2 - $fn0), ($fn3 - $fn1)]
} 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), .origin[1], (.origin[2] - 8)] 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)))
}]
}
' | sponge ./target/rp/models/blocks/geysercmd/${gid}.json
jq --arg gid "${gid}" --arg element_parent "${element_parent}" '.[$gid] += {"element_parent": $element_parent}' ./config.json | sponge config.json
geometryID=${gid}
printf "\e[33m[•]\e[m \e[37mGenerated new geometry for ${geometryID}\e[m\n"
else
geometryID=${matching_element}
printf "\e[33m[•]\e[m \e[37mUsing existing geometry from ${geometryID} for ${gid}\e[m\n"
jq --arg gid "${gid}" --arg geometryID "${geometryID}" '.[$gid].geometry = ("geometry.geysercmd." + $geometryID)' config.json | sponge config.json
fi
# get texture array
texture_array=($(jq -nr --argjson textures "${textures}" 'def namespace: if contains(":") then sub("\\:(.+)"; "") else "minecraft" end; {"textures": $textures} | .textures | to_entries | sort_by(.key) | map({(.key): .value}) | add | map("./assets/" + (. | namespace) + "/textures/" + (. | sub("(.*?)\\:"; "")) + ".png") | .[]'))
# crop any mcmeta associated files on the first frame and make sure we have a texture
tex_array_counter=0
for i in "${texture_array[@]}"
do
if test -f "${i}.mcmeta"; then
magick ${i} -background none -gravity North -extent "%[fx:h<w?h:w]x%[fx:h<w?h:w]" ${i}
printf "\e[31m[!]\e[m \e[37mCropped .textures[${tex_array_counter}] for use in ${gid} becuase mcmeta was detected\e[m\n"
rm "${i}.mcmeta"
fi
if ! test -f "${i}"; then
texture_array[${tex_array_counter}]="./assets/minecraft/textures/fallbacktexture.png"
printf "\e[31m[!]\e[m \e[37mFallback texture was used for ${gid}.textures[${tex_array_counter}]\e[m\n"
fi
tex_array_counter=$((tex_array_counter+1))
done
tile_dim=($(jq -nr --argjson textures "${textures}" '$textures | length | sqrt | ceil'))
# find widest image & cap texture_width @ 128
texture_width=($(identify -format "%w\n" ${texture_array[@]} | sort -n -r -k 1 | head -n 1))
texture_width=$(($texture_width <= ${maximum_width_dimension} ? $texture_width : ${maximum_width_dimension}))
# generate stitched texture with imagemagick (cap size @512) (perhaps we can filter to used textures only)
montage ${texture_array[@]} -background transparent -geometry x${texture_width}+0+0 -tile ${tile_dim}x${tile_dim} -interpolate Integer -filter point ./target/rp/textures/blocks/geysercmd/${gid}.png
# ensure we do not already have this texture, and reuse in the case that we do
texture_id=${gid}
texture_hash=($(md5 -q "./target/rp/textures/blocks/geysercmd/${gid}.png" 2>/dev/null))
match_count=($(md5 -r ./target/rp/textures/blocks/geysercmd/*.png 2>/dev/null | grep "${texture_hash}" | wc -l))
if [ ${match_count} -gt 1 ]
then
# remove our newly generated duplicate texture and change our texture_id to the match
rm "./target/rp/textures/blocks/geysercmd/${gid}.png"
texture_id=($(md5 -r ./target/rp/textures/blocks/geysercmd/*.png | grep "${texture_hash}" | cut -d "." -f 2 | cut -c38-))
printf "\e[33m[•]\e[m \e[37mReusing texture from ${texture_id} for ${gid}\e[m\n"
fi
# append our texture information to rp terrain_texture.json
jq -c --arg texture_id "${texture_id}" --arg geyser_id "${gid}" '
.texture_data += {
($geyser_id): {
"textures": ("textures/blocks/geysercmd/" + $texture_id)
}
}
' ./target/rp/textures/terrain_texture.json | sponge ./target/rp/textures/terrain_texture.json
# now gen animation, block, and attachable, taking care to use our newly defined geometryID
# generate our rp animations via display settings
jq -c -n --argjson display "${display}" --arg model_name "${gid}" '{"display": $display} |
{
"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)
' | 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 geometryID "${geometryID}" --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/" + $texture_name),
"enchanted": "textures/misc/enchanted_item_glint"
},
"geometry": {
"default": ("geometry.geysercmd." + $geometryID)
},
"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 geometryID "${geometryID}" --arg geyser_id "${gid}" '
{
"format_version": "1.16.200",
"minecraft:block": {
"description": {
"identifier": ("geysercmd:" + $geyser_id)
},
"components": {
"minecraft:material_instances": {
"*": {
"texture": $geyser_id,
"render_method": $block_material,
"face_dimming": false,
"ambient_occlusion": false
}
},
"tag:geysercmd:example_block": {},
"minecraft:geometry": ("geometry.geysercmd." + $geometryID),
"minecraft:placement_filter": {
"conditions": [
{
"allowed_faces": [
],
"block_filter": [
]
}
]
}
}
}
}
' | sponge ./target/bp/blocks/geysercmd/${gid}.json
printf "\e[32m[+]\e[m \e[37mChild ${gid} converted\e[m\n"
ProgressBar ${cur_pos} ${_end}
echo
else
printf "\e[37mSuitable parent information was not availbile for ${gid}...\e[m\n"
printf "\e[31m[X]\e[m \e[37mDeleting ${gid} from config\e[m\n"
ProgressBar ${cur_pos} ${_end}
echo
jq --arg gid "${gid}" 'del(.[$gid])' config.json | sponge config.json
fi
done < pa.csv
printf "\e[32m[+]\e[m \e[37mFinished conversion of child models\e[m\n"
echo
# write lang file US
printf "\e[33m[•]\e[m \e[37mWriting en_US and en_GB lang files\e[m\n"
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
printf "\e[32m[+]\e[m \e[37men_US and en_GB lang files written\e[m\n"
echo
# apply image compression if we can
if command -v convert >/dev/null 2>&1 ; then
printf "\e[32m[+]\e[m \e[37mOptional dependency pngquant detected\e[m\n"
printf "\e[33m[•]\e[m \e[37mAttempting image compression\e[m\n"
pngquant -f --skip-if-larger --ext .png --strip ./target/rp/textures/blocks/geysercmd/*.png
printf "\e[32m[+]\e[m \e[37mImage compression complete\e[m\n"
echo
fi
# attempt to merge with existing pack if input was provided
if test -f ${merge_input}; then
mkdir inputbedrockpack
printf "\e[33m[•]\e[m \e[37mDecompressing input bedrock pack\e[m\n"
unzip -q ${merge_input} -d ./inputbedrockpack
printf "\e[33m[•]\e[m \e[37mMerging input bedrock pack with generated bedrock assets\e[m\n"
cp -n -r "./inputbedrockpack"/* './target/rp/'
if test -f ./inputbedrockpack/textures/terrain_texture.json; then
printf "\e[33m[•]\e[m \e[37mMerging terrain texture files\e[m\n"
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
printf "\e[33m[•]\e[m \e[37mMerging languages file\e[m\n"
jq -s '.[0] + .[1] | unique' | sponge ./target/rp/texts/languages.json
fi
if test -f ./inputbedrockpack/texts/en_US.lang; then
printf "\e[33m[•]\e[m \e[37mMerging en_US lang file\e[m\n"
cat ./inputbedrockpack/texts/en_US.lang >> ./target/rp/texts/en_US.lang
fi
if test -f ./inputbedrockpack/texts/en_GB.lang; then
printf "\e[33m[•]\e[m \e[37mMerging en_GB lang file\e[m\n"
cat ./inputbedrockpack/texts/en_GB.lang >> ./target/rp/texts/en_GB.lang
fi
printf "\e[31m[X]\e[m \e[37mDeleting input bedrock pack scratch direcotry\e[m\n"
rm -rf inputbedrockpack
printf "\e[32m[+]\e[m \e[37mInput bedrock pack merged with generated assets\e[m\n"
echo
fi
# cleanup
printf "\e[31m[X]\e[m \e[37mDeleting scratch files\e[m\n"
rm -rf assets && rm -f pack.mcmeta && rm -f pack.png && rm -f parents.json && rm -f np.csv && rm -f pa.csv && rm -f README.md && rm -f README.txt
printf "\e[31m[X]\e[m \e[37mDeleting unused entries from config\e[m\n"
jq 'map_values(del(.path, .element_parent, .parent, .geyserID))' config.json | sponge config.json
printf "\e[33m[•]\e[m \e[37mMoving config to target directory\e[m\n"
mv config.json ./target/config.json
echo
printf "\e[33m[•]\e[m \e[37mCompressing output packs\e[m\n"
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"
echo
printf "\e[37mExiting...\e[m\n"
echo
@Kas-tle
Copy link
Author

Kas-tle commented Feb 15, 2021

Usage

To run, simply:

./converter.sh MyResourcePack.zip

To run without settings prompts and use the defaults:

./converter.sh MyResourcePack.zip default

About

The script has been updated to handle parent models. By default, it will downscale any input textures to a max width dimension of 128 to account for the lower expected graphics capacity of mobile devices. The extent of the scaling can be adjusted in the initial prompts.

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 out put 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 the beta version of Bedrock Edition (1.16.210.59+). Please be sure to enable experimental settings on world generation (Holiday Creator Features & Additional Modding Capabilities). 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:

/give @p geysercmd:gmdl_####
/replaceitem entity @p slot.armor.head 0 geysercmd:gmdl_####

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.

If you have pngquant installed, the script will also attempt image compression on the generated textures, though this is not entirely required.

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

Debian, Ubuntu, & Mint

sudo apt-get install moreutils jq imagemagick pngquant unzip zip

MacOS

brew install moreutils jq imagemagick pngquant unzip zip

RHEL, Fedora, & Centos

sudo yum install moreutils jq imagemagick pngquant unzip zip

Arch Linux

sudo pacman -S moreutils jq imagemagick pngquant unzip zip

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:

wget https://github.com/stedolan/jq/releases/download/jq-1.6/jq-linux64 && sudo chmod +x jq-linux64 && sudo mv jq-linux64 /usr/bin/jq

@bobhenl
Copy link

bobhenl commented Mar 25, 2021

Hi, I've tried to convert this pack but I wasn't successful :/ Could you please look at it too?

https://www.mediafire.com/folder/e6u9rgl0xu1o1/resourcepack

@Kas-tle
Copy link
Author

Kas-tle commented Mar 25, 2021

Hi, I've tried to convert this pack but I wasn't successful :/ Could you please look at it too?

https://www.mediafire.com/folder/e6u9rgl0xu1o1/resourcepack

I have updated the converter so that it will work if you only have a block or an item folder. That's why it was failing before... however, you also have a more serious problem. Despite this pack containing around 1400 models, for some inexplicable reason, you have about 50,000 predicates in this pack. Because the converter checks for valid predicates and must generate a custom block and attachable for each one in your pack, this is essentially untenable. Even if this resource pack could be converted, it would be near impossible to actually load in Bedrock. I suggest you go through this pack and remove all the duplicate predicate entries.

@LGCoding
Copy link

LGCoding commented Dec 7, 2021

Do the packs work with geyser yet or are they just for singleplayer.

@Kas-tle
Copy link
Author

Kas-tle commented Dec 7, 2021

@LGCoding just for single player at the moment. There are a couple of forks floating around right now for custom model data mappings, though I am only aware of one that has preliminary support for the block method utilized by this converter (though I am unsure if the source for that is available yet). We usually discuss progress in the development channel of the Geyser Discord.

@Kas-tle
Copy link
Author

Kas-tle commented Dec 7, 2021

Also will note I have a more modern version of this that uses pre-atlasing instead and has been refactored to be more readable, as well as utilize async operations with Bash 4 to speed up conversion time https://gist.github.com/Kas-tle/1ceffdcef37aaaeffe938d97ec522ea5

@LGCoding
Copy link

LGCoding commented Dec 8, 2021

when I use the new converter none of the models show up

@TypicalShavonne
Copy link

im stuck at
[•] Doing an initial sweep for level 1 parentals
like it just froze there, left it few mins already :d
image

@TypicalShavonne
Copy link

im stuck at [•] Doing an initial sweep for level 1 parentals like it just froze there, left it few mins already :d image

heres my pack https://github.com/TypicalShavonne/simpmc_rsp

@Kas-tle
Copy link
Author

Kas-tle commented Apr 6, 2022

im stuck at
[•] Doing an initial sweep for level 1 parentals
like it just froze there, left it few mins already :d
image

Please use the updated version of the converter https://gist.github.com/Kas-tle/1ceffdcef37aaaeffe938d97ec522ea5

@TypicalShavonne
Copy link

im stuck at
[•] Doing an initial sweep for level 1 parentals
like it just froze there, left it few mins already :d
image

Please use the updated version of the converter https://gist.github.com/Kas-tle/1ceffdcef37aaaeffe938d97ec522ea5
ok, it still stuck at that step 😂
image
im using WSL btw

@Kas-tle
Copy link
Author

Kas-tle commented Apr 6, 2022

@TypicalShavonne the pack you linked has no predicates so I'm a little confused. What are you trying to do with this?

@TypicalShavonne
Copy link

its a blank pack, i haven't added any models and predicates yet, do the converter need the predicates ?

@TypicalShavonne
Copy link

alright, it is fixed when i do put a model and predicates into the java pack, also,
You got any plan for this to support this fork? GeyserMC/Geyser#2822
I think it would work already since it is using namespace geysercmd:

@Kas-tle
Copy link
Author

Kas-tle commented Apr 6, 2022

@TypicalShavonne unfortunately that fork does not yet support the use of blocks; only items. That presents a major problem for automated 3D conversion as the item model cannot be used to render the inventory view. This is only possible for blocks. In order to automate it, I'd need some way to quickly render thumbnails of input items to use as the item sprite. Unfortunately I'm not aware of anything I could use for this at the moment. I would also have to change my mappings to resemble their format, but that should be fairly straightforward.

@TypicalShavonne
Copy link

ah ok, i see then :D

@OJASisLive
Copy link

any discord for support would be great

@TypicalShavonne
Copy link

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