Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Initial Blender 4.0+ support #114

Merged
merged 4 commits into from
Jul 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 10 additions & 4 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,18 @@ jobs:
strategy:
fail-fast: false
matrix:
bpy-version: ["3.6"] # TODO: add 4.0 when support is added
bpy: [{"bpy-version": "3.6", "python-version": "3.10"}, {"bpy-version": "4.1", "python-version": "3.11"}]
steps:
- uses: actions/checkout@v3
- name: Set up Python 3.10 with bpy ${{ matrix.bpy-version }}
- name: Set up Python ${{ matrix.bpy.python-version }} with bpy ${{ matrix.bpy-version }}
uses: actions/setup-python@v5
with:
python-version: '3.10'
python-version: ${{ matrix.bpy.python-version }}
cache: 'pip'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
python -m pip install bpy==${{ matrix.bpy-version }} -e .[test]
python -m pip install bpy==${{ matrix.bpy.bpy-version }} -e .[test]
- name: Lint with flake8
run: |
# stop the build if there are Python syntax errors or undefined names
Expand All @@ -37,8 +37,14 @@ jobs:
curl -L -o sample-re5-1.arc --output-dir $ALBAM_RE5_ARC_DIR '${{ secrets.SAMPLE_LINK_RE5_1 }}'
- name: Test with pytest # don't use xdist (-n auto) without checking coverage report
run: |
set +e
coverage run -m pytest --mtfw-dataset=tests/mtfw/datasets/ci.json --arcdir=re1::${ALBAM_RE1_ARC_DIR} --arcdir=re5::${ALBAM_RE5_ARC_DIR}
exitcode="$?"
# Ignore segfault in bpy==4.1.0; there's no way around it yet.
if [ $exitcode == 139 ];then exitcode=0;
fi
coverage json -o .coverage.json
exit "$exitcode"
- name: "Upload coverage data"
uses: actions/upload-artifact@v3
with:
Expand Down
86 changes: 45 additions & 41 deletions albam/engines/mtfw/material.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from kaitaistruct import KaitaiStream

from albam.exceptions import AlbamCheckFailure
from albam.lib.blender import get_bl_materials
from albam.lib.blender import get_bl_materials, ShaderGroupCompat
from albam.registry import blender_registry
from albam.vfs import VirtualFileData
from .defines import get_shader_objects
Expand Down Expand Up @@ -736,47 +736,51 @@ def _create_mtfw_shader():
group_inputs = shader_group.nodes.new("NodeGroupInput")
group_inputs.location = (-2000, -200)

bl_major, _, _ = bpy.app.version
compat = "OLD" if bl_major <= 3 else "NEW"

sg = ShaderGroupCompat(shader_group, compat)

# Create group inputs
shader_group.inputs.new("NodeSocketColor", "Diffuse BM")
shader_group.inputs.new("NodeSocketFloat", "Alpha BM")
shader_group.inputs["Alpha BM"].default_value = 1
shader_group.inputs.new("NodeSocketColor", "Albedo Blend BM")
shader_group.inputs.new("NodeSocketColor", "Albedo Blend 2 BM")
shader_group.inputs.new("NodeSocketColor", "Normal NM")
shader_group.inputs["Normal NM"].default_value = (1, 0.5, 1, 1)
shader_group.inputs.new("NodeSocketFloat", "Alpha NM")
shader_group.inputs["Alpha NM"].default_value = 0.5
shader_group.inputs.new("NodeSocketColor", "Specular MM")
shader_group.inputs.new("NodeSocketColor", "Lightmap LM")
shader_group.inputs.new("NodeSocketInt", "Use Lightmap")
shader_group.inputs["Use Lightmap"].min_value = 0
shader_group.inputs["Use Lightmap"].max_value = 1
shader_group.inputs.new("NodeSocketColor", "Alpha Mask AM")
shader_group.inputs.new("NodeSocketInt", "Use Alpha Mask")
shader_group.inputs["Use Alpha Mask"].min_value = 0
shader_group.inputs["Use Alpha Mask"].max_value = 1
shader_group.inputs.new("NodeSocketColor", "Environment CM")
shader_group.inputs.new("NodeSocketColor", "Detail DNM")
shader_group.inputs.new("NodeSocketColor", "Detail 2 DNM")
shader_group.inputs["Detail DNM"].default_value = (1, 0.5, 1, 1)
shader_group.inputs["Detail 2 DNM"].default_value = (1, 0.5, 1, 1)
shader_group.inputs.new("NodeSocketFloat", "Alpha DNM")
shader_group.inputs["Alpha DNM"].default_value = 0.5
shader_group.inputs.new("NodeSocketInt", "Use Detail Map")
shader_group.inputs["Use Detail Map"].min_value = 0
shader_group.inputs["Use Detail Map"].max_value = 1
shader_group.inputs.new("NodeSocketColor", "Special Map")
shader_group.inputs.new("NodeSocketString", "Special Map type")
shader_group.inputs.new("NodeSocketColor", "Vertex Displacement") # TODO: Try to use it in Blender
shader_group.inputs.new("NodeSocketColor", "Vertex Displacement Mask") # TODO: Try to use it in Blender
shader_group.inputs.new("NodeSocketColor", "Hair Shift") # TODO: Try to use it in Blender
shader_group.inputs.new("NodeSocketColor", "Height Map") # TODO: Try to use it in Blender
shader_group.inputs.new("NodeSocketColor", "Emission") # TODO: Try to use it in Blender
sg.new_socket("Diffuse BM", in_out="INPUT", socket_type="NodeSocketColor")
sg.new_socket("Alpha BM", in_out="INPUT", socket_type="NodeSocketFloat")
sg.inputs["Alpha BM"].default_value = 1
sg.new_socket("Albedo Blend BM", in_out="INPUT", socket_type="NodeSocketColor")
sg.new_socket("Albedo Blend 2 BM", in_out="INPUT", socket_type="NodeSocketColor", )
sg.new_socket("Normal NM", in_out="INPUT", socket_type="NodeSocketColor")
sg.inputs["Normal NM"].default_value = (1, 0.5, 1, 1)
sg.new_socket("Alpha NM", in_out="INPUT", socket_type="NodeSocketFloat", )
sg.inputs["Alpha NM"].default_value = 0.5
sg.new_socket("Specular MM", in_out="INPUT", socket_type="NodeSocketColor")
sg.new_socket("Lightmap LM", in_out="INPUT", socket_type="NodeSocketColor")
sg.new_socket("Use Lightmap", in_out="INPUT", socket_type="NodeSocketInt")
sg.inputs["Use Lightmap"].min_value = 0
sg.inputs["Use Lightmap"].max_value = 1
sg.new_socket("Alpha Mask AM", in_out="INPUT", socket_type="NodeSocketColor")
sg.new_socket("Use Alpha Mask", in_out="INPUT", socket_type="NodeSocketInt")
sg.inputs["Use Alpha Mask"].min_value = 0
sg.inputs["Use Alpha Mask"].max_value = 1
sg.new_socket("Environment CM", in_out="INPUT", socket_type="NodeSocketColor")
sg.new_socket("Detail DNM", in_out="INPUT", socket_type="NodeSocketColor")
sg.new_socket("Detail 2 DNM", in_out="INPUT", socket_type="NodeSocketColor")
sg.inputs["Detail DNM"].default_value = (1, 0.5, 1, 1)
sg.inputs["Detail 2 DNM"].default_value = (1, 0.5, 1, 1)
sg.new_socket("Alpha DNM", in_out="INPUT", socket_type="NodeSocketFloat")
sg.inputs["Alpha DNM"].default_value = 0.5
sg.new_socket("Use Detail Map", in_out="INPUT", socket_type="NodeSocketInt")
sg.inputs["Use Detail Map"].min_value = 0
sg.inputs["Use Detail Map"].max_value = 1
sg.new_socket("Special Map", in_out="INPUT", socket_type="NodeSocketColor")
sg.new_socket("Vertex Displacement", in_out="INPUT", socket_type="NodeSocketColor")
sg.new_socket("Vertex Displacement Mask", in_out="INPUT", socket_type="NodeSocketColor")
sg.new_socket("Hair Shift", in_out="INPUT", socket_type="NodeSocketColor")
sg.new_socket("Height Map", in_out="INPUT", socket_type="NodeSocketColor")
sg.new_socket("Emission", in_out="INPUT", socket_type="NodeSocketColor", )

# Create group outputs
group_outputs = shader_group.nodes.new("NodeGroupOutput")
group_outputs.location = (300, -90)
shader_group.outputs.new("NodeSocketShader", "Surface")
sg.new_socket("Surface", in_out="OUTPUT", socket_type="NodeSocketShader")

# Shader node
bsdf_shader = shader_group.nodes.new("ShaderNodeBsdfPrincipled")
Expand Down Expand Up @@ -892,9 +896,9 @@ def _create_mtfw_shader():
link(group_inputs.outputs["Diffuse BM"], multiply_diff_light.inputs[1])
link(multiply_diff_light.outputs[0], use_lightmap.inputs[2])
link(group_inputs.outputs["Diffuse BM"], use_lightmap.inputs[1])
link(use_lightmap.outputs[0], bsdf_shader.inputs[0])
link(use_lightmap.outputs[0], bsdf_shader.inputs["Base Color"])
link(group_inputs.outputs["Alpha BM"], use_alpha_mask.inputs[1])
link(use_alpha_mask.outputs[0], bsdf_shader.inputs[21])
link(use_alpha_mask.outputs[0], bsdf_shader.inputs["Alpha"])
link(group_inputs.outputs["Normal NM"], normal_separate.inputs[0])
link(normal_separate.outputs[1], normal_combine.inputs[1])
link(normal_separate.outputs[2], normal_combine.inputs[2])
Expand All @@ -903,7 +907,7 @@ def _create_mtfw_shader():
link(normal_combine.outputs[0], separate_rgb_n.inputs[0])

link(group_inputs.outputs["Specular MM"], invert_spec.inputs[1])
link(invert_spec.outputs[0], bsdf_shader.inputs[9])
link(invert_spec.outputs[0], bsdf_shader.inputs["Roughness"])
link(group_inputs.outputs["Lightmap LM"], multiply_diff_light.inputs[2])
link(group_inputs.outputs["Use Lightmap"], use_lightmap.inputs[0])
link(group_inputs.outputs["Alpha Mask AM"], use_alpha_mask.inputs[2]) # use alpha mask > color 2
Expand All @@ -929,7 +933,7 @@ def _create_mtfw_shader():
link(normalize_normals.outputs[0], use_detail_map.inputs[2])
link(use_detail_map.outputs[0], invert_green.inputs[1])
link(invert_green.outputs[0], normal_map.inputs[1])
link(normal_map.outputs[0], bsdf_shader.inputs[22])
link(normal_map.outputs[0], bsdf_shader.inputs["Normal"])
link(group_inputs.outputs["Use Detail Map"], use_detail_map.inputs[0])

return shader_group
Expand Down
14 changes: 11 additions & 3 deletions albam/engines/mtfw/mesh.py
Original file line number Diff line number Diff line change
Expand Up @@ -581,17 +581,25 @@ def _get_weights(mod, mesh, vertex):
def _build_normals(bl_mesh, normals):
if not normals:
return
bl_mesh.create_normals_split()
try:
bl_mesh.create_normals_split()
except AttributeError:
# blender 4.1+
pass
bl_mesh.validate(clean_customdata=False)
bl_mesh.update(calc_edges=True)
bl_mesh.polygons.foreach_set("use_smooth", [True] * len(bl_mesh.polygons))
# bl_mesh.polygons.foreach_set("use_smooth", [True] * len(bl_mesh.polygons))

vert_normals = np.array(normals, dtype=np.float32)
norms = np.linalg.norm(vert_normals, axis=1, keepdims=True)
np.divide(vert_normals, norms, out=vert_normals, where=norms != 0)

bl_mesh.normals_split_custom_set_from_vertices(vert_normals)
bl_mesh.use_auto_smooth = True
try:
bl_mesh.use_auto_smooth = True
except AttributeError:
# blender 4.1+
pass


def _build_uvs(bl_mesh, uvs, name="uv"):
Expand Down
2 changes: 0 additions & 2 deletions albam/engines/mtfw/texture.py
Original file line number Diff line number Diff line change
Expand Up @@ -349,8 +349,6 @@ def texture_code_to_blender_texture(texture_code, blender_texture_node, blender_
# Lightmap with Alpha mask in Re5
blender_texture_node.location = (-300, -1050)
link(blender_texture_node.outputs["Color"], shader_node_grp.inputs["Special Map"])
# TODO set a proper string value or remove
shader_node_grp.inputs["Special Map type"].default_value = str(tex_unk_type)

elif texture_code == 6:
# Alpha mask _AM
Expand Down
29 changes: 28 additions & 1 deletion albam/lib/blender.py
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,11 @@ def get_normals_per_vertex(blender_mesh):
normals = {}

if blender_mesh.has_custom_normals:
blender_mesh.calc_normals_split()
try:
blender_mesh.calc_normals_split()
except AttributeError:
# blender 4.1+
pass
for loop in blender_mesh.loops:
normals.setdefault(loop.vertex_index, loop.normal)
else:
Expand Down Expand Up @@ -315,3 +319,26 @@ def get_dist(point_a, point_b):
z3 = z1 - z2
magnitude = math.sqrt((x3 * x3) + (y3 * y3) + (z3 * z3))
return magnitude


class ShaderGroupCompat:

def __init__(self, shader_group, compat="NEW"):
self.shader_group = shader_group
self.compat = compat

def new_socket(self, name, description="", in_out='INPUT', socket_type='DEFAULT', parent=None):
if self.compat == "NEW":
return self.shader_group.interface.new_socket(
name, description=description, in_out=in_out, socket_type=socket_type, parent=parent)
elif in_out == "INPUT":
return self.shader_group.inputs.new(socket_type, name)
elif in_out == "OUTPUT":
return self.shader_group.outputs.new(socket_type, name)

@property
def inputs(self):
if self.compat != "NEW":
return self.shader_group.inputs
return {item.name: item for item in self.shader_group.interface.items_tree
if item.item_type == "SOCKET" and item.in_out == "INPUT"}
5 changes: 3 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@ version = "0.3.6"
description = "Import 3d models into Blender"
readme = "README.md"
authors = [ {name = "Sebastian A. Brachi"} ]
requires-python = "==3.10.*"
requires-python = ">=3.10,<3.12"
license = {file = "LICENSE"}
keywords = ["blender", "blender-addon", "import", "3d models", "3d formats"]

dependencies = [
"bpy==3.6",
'bpy == 3.6.0; python_version == "3.10.*"',
'bpy == 4.1.0; python_version == "3.11.*"',
]

[project.optional-dependencies]
Expand Down
Loading