Begin true material builder
This commit is contained in:
parent
5eb7221953
commit
f4898a130c
|
@ -173,10 +173,6 @@
|
|||
"name": "lightColor",
|
||||
"link": "_lampColor"
|
||||
},
|
||||
{
|
||||
"name": "lightStrength",
|
||||
"link": "_lampStrength"
|
||||
},
|
||||
{
|
||||
"name": "eye",
|
||||
"link": "_cameraPosition"
|
||||
|
@ -366,10 +362,6 @@
|
|||
"name": "lightColor",
|
||||
"link": "_lampColor"
|
||||
},
|
||||
{
|
||||
"name": "lightStrength",
|
||||
"link": "_lampStrength"
|
||||
},
|
||||
{
|
||||
"name": "spotlightCutoff",
|
||||
"link": "_spotlampCutoff"
|
||||
|
@ -399,10 +391,6 @@
|
|||
"name": "lightColor",
|
||||
"link": "_lampColor"
|
||||
},
|
||||
{
|
||||
"name": "lightStrength",
|
||||
"link": "_lampStrength"
|
||||
},
|
||||
{
|
||||
"name": "shirr",
|
||||
"link": "_envmapIrradiance"
|
|
@ -120,7 +120,7 @@ void main() {
|
|||
indirect += prefilteredColor * (f0 * envBRDF.x + envBRDF.y);
|
||||
#endif
|
||||
|
||||
indirect = indirect * envmapStrength;// * lightColor * lightStrength;
|
||||
indirect = indirect * envmapStrength;// * lightColor;
|
||||
indirect = indirect * g1.a; // Occlusion
|
||||
|
||||
#ifdef _SSAO
|
||||
|
|
|
@ -53,7 +53,7 @@ uniform vec3 lightPos;
|
|||
uniform vec3 lightDir;
|
||||
uniform int lightType;
|
||||
// uniform int lightIndex;
|
||||
uniform float lightStrength;
|
||||
uniform vec3 lightColor;
|
||||
uniform float shadowsBias;
|
||||
uniform float spotlightCutoff;
|
||||
uniform float spotlightExponent;
|
||||
|
@ -69,8 +69,6 @@ uniform vec3 eye;
|
|||
|
||||
#ifdef _LampColTex
|
||||
uniform sampler2D texlampcolor;
|
||||
#else
|
||||
uniform vec3 lightColor;
|
||||
#endif
|
||||
|
||||
// in vec2 texCoord;
|
||||
|
@ -225,13 +223,11 @@ void main() {
|
|||
// vec3 direct = diffuseBRDF(albedo, metrough.y, dotNV, dotNL, dotVH, dotLV) + wardSpecular(n, h, dotNL, dotNV, dotNH, fiberDirection, shinyParallel, shinyPerpendicular);
|
||||
// #endif
|
||||
|
||||
direct *= lightStrength;
|
||||
direct *= lightColor;
|
||||
|
||||
#ifdef _LampColTex
|
||||
// direct *= texture(texlampcolor, envMapEquirect(l)).rgb;
|
||||
direct *= pow(texture(texlampcolor, l.xy).rgb, vec3(2.2));
|
||||
#else
|
||||
direct *= lightColor;
|
||||
direct *= pow(texture(texlampcolor, l.xy).rgb, vec3(2.2));
|
||||
#endif
|
||||
|
||||
#ifdef _SSS
|
||||
|
|
|
@ -35,18 +35,13 @@
|
|||
},
|
||||
{
|
||||
"name": "lightColor",
|
||||
"link": "_lampColor",
|
||||
"ifndef": ["_LampColTex"]
|
||||
"link": "_lampColor"
|
||||
},
|
||||
{
|
||||
"name": "texlampcolor",
|
||||
"link": "_lampColorTexture",
|
||||
"ifdef": ["_LampColTex"]
|
||||
},
|
||||
{
|
||||
"name": "lightStrength",
|
||||
"link": "_lampStrength"
|
||||
},
|
||||
{
|
||||
"name": "shadowsBias",
|
||||
"link": "_lampShadowsBias"
|
|
@ -33,10 +33,6 @@
|
|||
"name": "lightColor",
|
||||
"link": "_lampColor"
|
||||
},
|
||||
{
|
||||
"name": "lightStrength",
|
||||
"link": "_lampStrength"
|
||||
},
|
||||
{
|
||||
"name": "lightBias",
|
||||
"link": "_lampBias"
|
|
@ -39,10 +39,6 @@
|
|||
"name": "lightColor",
|
||||
"link": "_lampColor"
|
||||
},
|
||||
{
|
||||
"name": "lightStrength",
|
||||
"link": "_lampStrength"
|
||||
},
|
||||
{
|
||||
"name": "shadowsBias",
|
||||
"link": "_lampShadowsBias"
|
||||
|
@ -188,10 +184,6 @@
|
|||
"name": "lightColor",
|
||||
"link": "_lampColor"
|
||||
},
|
||||
{
|
||||
"name": "lightStrength",
|
||||
"link": "_lampStrength"
|
||||
},
|
||||
{
|
||||
"name": "shadowsBias",
|
||||
"link": "_lampShadowsBias"
|
||||
|
@ -331,10 +323,6 @@
|
|||
"name": "lightColor",
|
||||
"link": "_lampColor"
|
||||
},
|
||||
{
|
||||
"name": "lightStrength",
|
||||
"link": "_lampStrength"
|
||||
},
|
||||
{
|
||||
"name": "eye",
|
||||
"link": "_cameraPosition"
|
||||
|
@ -479,10 +467,6 @@
|
|||
"name": "lightColor",
|
||||
"link": "_lampColor"
|
||||
},
|
||||
{
|
||||
"name": "lightStrength",
|
||||
"link": "_lampStrength"
|
||||
},
|
||||
{
|
||||
"name": "spotlightCutoff",
|
||||
"link": "_spotlampCutoff"
|
||||
|
@ -512,10 +496,6 @@
|
|||
"name": "lightColor",
|
||||
"link": "_lampColor"
|
||||
},
|
||||
{
|
||||
"name": "lightStrength",
|
||||
"link": "_lampStrength"
|
||||
},
|
||||
{
|
||||
"name": "shirr",
|
||||
"link": "_envmapIrradiance"
|
|
@ -76,7 +76,6 @@ uniform vec3 lightPos;
|
|||
uniform vec3 lightDir;
|
||||
uniform int lightType;
|
||||
uniform vec3 lightColor;
|
||||
uniform float lightStrength;
|
||||
uniform float spotlightCutoff;
|
||||
uniform float spotlightExponent;
|
||||
uniform float shadowsBias;
|
||||
|
@ -353,7 +352,7 @@ void main() {
|
|||
}
|
||||
#endif
|
||||
|
||||
direct = direct * lightColor * lightStrength;
|
||||
direct = direct * lightColor;
|
||||
|
||||
|
||||
#ifdef _VoxelGI
|
||||
|
@ -401,7 +400,7 @@ void main() {
|
|||
vec3 indirectSpecular = prefilteredColor * (f0 * envBRDF.x + envBRDF.y);
|
||||
indirect += indirectSpecular;
|
||||
#endif
|
||||
indirect = indirect * envmapStrength;// * lightColor * lightStrength;
|
||||
indirect = indirect * envmapStrength;// * lightColor;
|
||||
|
||||
#endif
|
||||
|
||||
|
|
|
@ -44,10 +44,6 @@
|
|||
"name": "lightColor",
|
||||
"link": "_lampColor"
|
||||
},
|
||||
{
|
||||
"name": "lightStrength",
|
||||
"link": "_lampStrength"
|
||||
},
|
||||
{
|
||||
"name": "shadowsBias",
|
||||
"link": "_lampShadowsBias"
|
||||
|
@ -154,10 +150,6 @@
|
|||
"name": "lightColor",
|
||||
"link": "_lampColor"
|
||||
},
|
||||
{
|
||||
"name": "lightStrength",
|
||||
"link": "_lampStrength"
|
||||
},
|
||||
{
|
||||
"name": "shadowsBias",
|
||||
"link": "_lampShadowsBias"
|
||||
|
@ -257,10 +249,6 @@
|
|||
"name": "lightColor",
|
||||
"link": "_lampColor"
|
||||
},
|
||||
{
|
||||
"name": "lightStrength",
|
||||
"link": "_lampStrength"
|
||||
},
|
||||
{
|
||||
"name": "eye",
|
||||
"link": "_cameraPosition"
|
||||
|
@ -469,10 +457,6 @@
|
|||
"name": "lightColor",
|
||||
"link": "_lampColor"
|
||||
},
|
||||
{
|
||||
"name": "lightStrength",
|
||||
"link": "_lampStrength"
|
||||
},
|
||||
{
|
||||
"name": "spotlightCutoff",
|
||||
"link": "_spotlampCutoff"
|
||||
|
@ -502,10 +486,6 @@
|
|||
"name": "lightColor",
|
||||
"link": "_lampColor"
|
||||
},
|
||||
{
|
||||
"name": "lightStrength",
|
||||
"link": "_lampStrength"
|
||||
},
|
||||
{
|
||||
"name": "shirr",
|
||||
"link": "_envmapIrradiance"
|
|
@ -77,7 +77,6 @@ uniform vec3 lightPos;
|
|||
uniform vec3 lightDir;
|
||||
uniform int lightType;
|
||||
uniform vec3 lightColor;
|
||||
uniform float lightStrength;
|
||||
uniform float shadowsBias;
|
||||
uniform float spotlightCutoff;
|
||||
uniform float spotlightExponent;
|
||||
|
@ -212,7 +211,7 @@ void main() {
|
|||
}
|
||||
}
|
||||
|
||||
direct = direct * lightColor * lightStrength;
|
||||
direct = direct * lightColor;
|
||||
|
||||
// Indirect
|
||||
vec3 indirectDiffuse = shIrradiance(n, 2.2) / PI;
|
||||
|
@ -233,7 +232,7 @@ void main() {
|
|||
vec3 indirectSpecular = prefilteredColor * (f0 * envBRDF.x + envBRDF.y);
|
||||
indirect += indirectSpecular;
|
||||
#endif
|
||||
indirect = indirect * envmapStrength; // * lightColor * lightStrength;
|
||||
indirect = indirect * envmapStrength; // * lightColor;
|
||||
outputColor = vec4(vec3(direct * visibility + indirect), 1.0);
|
||||
|
||||
#ifdef _OccTex
|
||||
|
|
|
@ -65,7 +65,6 @@ uniform float envmapStrength;
|
|||
uniform bool receiveShadow;
|
||||
uniform vec3 lightDir;
|
||||
uniform vec3 lightColor;
|
||||
uniform float lightStrength;
|
||||
|
||||
in vec3 position;
|
||||
#ifdef _Tex
|
||||
|
@ -150,7 +149,7 @@ void main() {
|
|||
#else
|
||||
vec3 direct = lambertDiffuseBRDF(albedo, dotNL) + specularBRDF(f0, roughness, dotNL, dotNH, dotNV, dotVH);
|
||||
#endif
|
||||
direct = direct * lightColor * lightStrength;
|
||||
direct = direct * lightColor;
|
||||
|
||||
// Indirect
|
||||
vec3 indirectDiffuse = shIrradiance(n, 2.2) / PI;
|
||||
|
@ -171,7 +170,7 @@ void main() {
|
|||
vec3 indirectSpecular = prefilteredColor * (f0 * envBRDF.x + envBRDF.y);
|
||||
indirect += indirectSpecular;
|
||||
#endif
|
||||
indirect = indirect * envmapStrength; // * lightColor * lightStrength;
|
||||
indirect = indirect * envmapStrength; // * lightColor;
|
||||
fragColor = vec4(vec3(direct * visibility + indirect), 1.0);
|
||||
|
||||
#ifdef _OccTex
|
||||
|
|
|
@ -74,7 +74,6 @@ uniform vec3 lightPos;
|
|||
uniform vec3 lightDir;
|
||||
uniform int lightType;
|
||||
uniform vec3 lightColor;
|
||||
uniform float lightStrength;
|
||||
uniform float spotlightCutoff;
|
||||
uniform float spotlightExponent;
|
||||
uniform vec3 eye;
|
||||
|
@ -194,7 +193,7 @@ void main() {
|
|||
}
|
||||
}
|
||||
|
||||
direct = direct * lightColor * lightStrength;
|
||||
direct = direct * lightColor;
|
||||
|
||||
// Indirect
|
||||
vec3 indirectDiffuse = shIrradiance(n, 2.2) / PI;
|
||||
|
@ -215,7 +214,7 @@ void main() {
|
|||
vec3 indirectSpecular = prefilteredColor * (f0 * envBRDF.x + envBRDF.y);
|
||||
indirect += indirectSpecular;
|
||||
#endif
|
||||
indirect = indirect * envmapStrength;// * lightColor * lightStrength;
|
||||
indirect = indirect * envmapStrength;// * lightColor;
|
||||
|
||||
|
||||
vec4 premultipliedReflect = vec4(vec3(direct * visibility + indirect * occlusion), matColor.a);
|
||||
|
|
|
@ -21,7 +21,6 @@ uniform vec3 viewPos;
|
|||
uniform vec3 lightPos;
|
||||
uniform vec3 lightColor;
|
||||
uniform float lightRadius;
|
||||
uniform float lightStrength;
|
||||
uniform float shadowsBias;
|
||||
|
||||
in vec4 wvpposition;
|
||||
|
|
|
@ -42,10 +42,6 @@
|
|||
"name": "lightColor",
|
||||
"link": "_lampColor"
|
||||
},
|
||||
{
|
||||
"name": "lightStrength",
|
||||
"link": "_lampStrength"
|
||||
},
|
||||
{
|
||||
"name": "snoise",
|
||||
"link": "_noise8"
|
|
@ -22,6 +22,7 @@ import armutils
|
|||
import subprocess
|
||||
import log
|
||||
import make_material
|
||||
import material.make as make_material_full
|
||||
import nodes
|
||||
|
||||
NodeTypeNode = 0
|
||||
|
@ -2756,7 +2757,10 @@ class ArmoryExporter:
|
|||
|
||||
# Parse from material output
|
||||
if decal_uv_layer == None:
|
||||
make_material.parse(self, material, c, defs)
|
||||
if wrd.arm_material_level == 'Restricted':
|
||||
make_material.parse(self, material, c, defs)
|
||||
else:
|
||||
make_material_full.parse(self, material, c, defs)
|
||||
o['contexts'].append(c)
|
||||
# Decal attached, split material into two separate ones
|
||||
# Mandatory starting point from mix node for now
|
||||
|
@ -2928,7 +2932,10 @@ class ArmoryExporter:
|
|||
with_tess = True
|
||||
if wrd.voxelgi:
|
||||
geom_context = 'voxel'
|
||||
self.finalize_shader(o, defs, ArmoryExporter.renderpath_passes, with_tess=with_tess, geom_context=geom_context)
|
||||
if wrd.arm_material_level == 'Restricted':
|
||||
self.finalize_shader(o, defs, ArmoryExporter.renderpath_passes, with_tess=with_tess, geom_context=geom_context)
|
||||
else:
|
||||
self.finalize_shader_full(o, material, ArmoryExporter.renderpath_passes)
|
||||
else:
|
||||
# TODO: gather defs from vertex data when custom shader is used
|
||||
o['shader'] = material.override_shader_name
|
||||
|
@ -3111,7 +3118,17 @@ class ArmoryExporter:
|
|||
po['volume'] = volume
|
||||
po['volume_center'] = volume_center
|
||||
return po
|
||||
|
||||
|
||||
def finalize_shader_full(self, o, material, rpasses):
|
||||
shader_data_name = material.name + '_data'
|
||||
shader_data_path = 'build/compiled/ShaderRaws/' + material.name + '/' + shader_data_name + '.arm'
|
||||
assets.add_shader_data(shader_data_path)
|
||||
o['shader'] = shader_data_name + '/' + shader_data_name
|
||||
for ren_pass in rpasses:
|
||||
full_name = 'build/compiled/ShaderRaws/' + material.name + '/' + material.name + '_' + ren_pass
|
||||
assets.add_shader(full_name + '.vert.glsl')
|
||||
assets.add_shader(full_name + '.frag.glsl')
|
||||
|
||||
def finalize_shader(self, o, defs, renderpath_passes, with_tess=False, geom_context=None):
|
||||
# Merge duplicates and sort
|
||||
defs = sorted(list(set(defs)))
|
||||
|
|
|
@ -28,7 +28,7 @@ def compile_shader(raw_shaders_path, shader_name, defs):
|
|||
fp = armutils.get_fp()
|
||||
|
||||
# Open json file
|
||||
json_name = shader_name + '.shader.json'
|
||||
json_name = shader_name + '.json'
|
||||
base_name = json_name.split('.', 1)[0]
|
||||
with open(json_name) as f:
|
||||
json_file = f.read()
|
||||
|
@ -43,6 +43,24 @@ def export_data(fp, sdk_path, is_play=False, is_publish=False, in_viewport=False
|
|||
|
||||
print('\nArmory v' + wrd.arm_version)
|
||||
|
||||
# Clean compiled variants if cache is disabled
|
||||
if wrd.arm_cache_shaders == False:
|
||||
if os.path.isdir('build/html5-resources'):
|
||||
shutil.rmtree('build/html5-resources')
|
||||
if os.path.isdir('build/krom-resources'):
|
||||
shutil.rmtree('build/krom-resources')
|
||||
if os.path.isdir('build/window/krom-resources'):
|
||||
shutil.rmtree('build/window/krom-resources')
|
||||
if os.path.isdir('build/compiled/Shaders'):
|
||||
shutil.rmtree('build/compiled/Shaders')
|
||||
if os.path.isdir('build/compiled/ShaderDatas'):
|
||||
shutil.rmtree('build/compiled/ShaderDatas')
|
||||
if os.path.isdir('build/compiled/ShaderRaws'):
|
||||
shutil.rmtree('build/compiled/ShaderRaws')
|
||||
# Remove shader datas if shaders were deleted
|
||||
elif os.path.isdir('build/compiled/Shaders') == False and os.path.isdir('build/compiled/ShaderDatas') == True:
|
||||
shutil.rmtree('build/compiled/ShaderDatas')
|
||||
|
||||
raw_shaders_path = sdk_path + 'armory/Shaders/'
|
||||
assets_path = sdk_path + 'armory/Assets/'
|
||||
export_physics = bpy.data.worlds['Arm'].arm_physics != 'Disabled'
|
||||
|
@ -83,22 +101,6 @@ def export_data(fp, sdk_path, is_play=False, is_publish=False, in_viewport=False
|
|||
|
||||
if navigation_found == False:
|
||||
export_navigation = False
|
||||
|
||||
# Clean compiled variants if cache is disabled
|
||||
if wrd.arm_cache_shaders == False:
|
||||
if os.path.isdir('build/html5-resources'):
|
||||
shutil.rmtree('build/html5-resources')
|
||||
if os.path.isdir('build/krom-resources'):
|
||||
shutil.rmtree('build/krom-resources')
|
||||
if os.path.isdir('build/window/krom-resources'):
|
||||
shutil.rmtree('build/window/krom-resources')
|
||||
if os.path.isdir('build/compiled/Shaders'):
|
||||
shutil.rmtree('build/compiled/Shaders')
|
||||
if os.path.isdir('build/compiled/ShaderDatas'):
|
||||
shutil.rmtree('build/compiled/ShaderDatas')
|
||||
# Remove shader datas if shaders were deleted
|
||||
elif os.path.isdir('build/compiled/Shaders') == False and os.path.isdir('build/compiled/ShaderDatas') == True:
|
||||
shutil.rmtree('build/compiled/ShaderDatas')
|
||||
|
||||
# Write referenced shader variants
|
||||
for ref in assets.shader_datas:
|
||||
|
|
0
blender/material/__init__.py
Executable file
0
blender/material/__init__.py
Executable file
64
blender/material/make.py
Normal file
64
blender/material/make.py
Normal file
|
@ -0,0 +1,64 @@
|
|||
import armutils
|
||||
import os
|
||||
import exporter
|
||||
from material.shader_data import ShaderData
|
||||
import material.make_forward as make_forward
|
||||
import material.state as state
|
||||
|
||||
def parse(self, material, mat_context, defs):
|
||||
state.material = material
|
||||
state.group = material.node_tree
|
||||
state.nodes = state.group.nodes
|
||||
state.links = state.group.links
|
||||
state.mat_context = mat_context
|
||||
state.defs = defs
|
||||
|
||||
state.path = armutils.get_fp() + '/build/compiled/ShaderRaws/' + material.name
|
||||
if not os.path.exists(state.path):
|
||||
os.makedirs(state.path)
|
||||
|
||||
state.data = ShaderData(material)
|
||||
|
||||
rid = exporter.ArmoryExporter.renderpath_id
|
||||
if rid == 'forward':
|
||||
parse_forward()
|
||||
elif rid == 'deferred':
|
||||
parse_deferred
|
||||
|
||||
# TODO: Merge finalize shader here..
|
||||
armutils.write_arm(state.path + '/' + material.name + '_data.arm', state.data.get())
|
||||
|
||||
def parse_deferred():
|
||||
pass
|
||||
|
||||
def parse_forward():
|
||||
rpasses = exporter.ArmoryExporter.renderpath_passes
|
||||
mesh_context_id = exporter.ArmoryExporter.mesh_context
|
||||
shadows_context_id = exporter.ArmoryExporter.shadows_context
|
||||
|
||||
for rp in rpasses:
|
||||
if rp == mesh_context_id:
|
||||
con = make_forward.mesh(rp)
|
||||
elif rp == shadows_context_id:
|
||||
con = make_forward.shadows(rp)
|
||||
else:
|
||||
continue
|
||||
|
||||
write_shaders(con, rp)
|
||||
|
||||
def write_shaders(con, rp):
|
||||
if con.vert != None:
|
||||
with open(state.path + '/' + state.material.name + '_' + rp + '.vert.glsl', 'w') as f:
|
||||
f.write(con.vert.get())
|
||||
if con.frag != None:
|
||||
with open(state.path + '/' + state.material.name + '_' + rp + '.frag.glsl', 'w') as f:
|
||||
f.write(con.frag.get())
|
||||
if con.geom != None:
|
||||
with open(state.path + '/' + state.material.name + '_' + rp + '.geom.glsl', 'w') as f:
|
||||
f.write(con.geom.get())
|
||||
if con.tesc != None:
|
||||
with open(state.path + '/' + state.material.name + '_' + rp + '.tesc.glsl', 'w') as f:
|
||||
f.write(con.tesc.get())
|
||||
if con.tese != None:
|
||||
with open(state.path + '/' + state.material.name + '_' + rp + '.tese.glsl', 'w') as f:
|
||||
f.write(con.tese.get())
|
0
blender/material/make_cycles.py
Normal file
0
blender/material/make_cycles.py
Normal file
478
blender/material/make_forward.py
Normal file
478
blender/material/make_forward.py
Normal file
|
@ -0,0 +1,478 @@
|
|||
import material.state as state
|
||||
|
||||
def node_by_type(ntype):
|
||||
for n in state.nodes:
|
||||
if n.type == ntype:
|
||||
return n
|
||||
|
||||
def mesh(context_id):
|
||||
# global parsed
|
||||
global frag
|
||||
global vert
|
||||
# global first_basecol # Do not multiply vals first time
|
||||
# global first_roughness
|
||||
# global first_metallic
|
||||
# parsed = [] # Compute node only onces
|
||||
# first_basecol = True
|
||||
# first_roughness = True
|
||||
# first_metallic = True
|
||||
|
||||
con_mesh = state.data.add_context({ 'name': context_id, 'depth_write': True, 'compare_mode': 'less', 'cull_mode': 'clockwise' })
|
||||
|
||||
vert = con_mesh.vert()
|
||||
frag = con_mesh.frag()
|
||||
|
||||
vert.add_out('vec3 wnormal')
|
||||
vert.add_out('vec3 wposition')
|
||||
vert.add_out('vec3 eyeDir')
|
||||
vert.add_uniform('mat4 W', '_worldMatrix')
|
||||
vert.add_uniform('mat4 N', '_normalMatrix')
|
||||
vert.add_uniform('mat4 WVP', '_worldViewProjectionMatrix')
|
||||
vert.add_uniform('vec3 eye', '_cameraPosition')
|
||||
vert.write('vec4 spos = vec4(pos, 1.0);')
|
||||
vert.write('wnormal = normalize(mat3(N) * nor);')
|
||||
vert.write('wposition = vec4(W * spos).xyz;')
|
||||
vert.write('eyeDir = eye - wposition;')
|
||||
vert.write('gl_Position = WVP * spos;')
|
||||
|
||||
frag.add_include('../../Shaders/compiled.glsl')
|
||||
frag.add_include('../../Shaders/std/brdf.glsl')
|
||||
frag.add_include('../../Shaders/std/math.glsl')
|
||||
frag.add_include('../../Shaders/std/shirr.glsl')
|
||||
frag.add_uniform('vec3 lightColor', '_lampColor')
|
||||
frag.add_uniform('vec3 lightDir', '_lampDirection')
|
||||
frag.add_uniform('vec3 lightPos', '_lampPosition')
|
||||
frag.add_uniform('int lightType', '_lampType')
|
||||
frag.add_uniform('float shirr[27]', link='_envmapIrradiance', included=True)
|
||||
frag.add_uniform('float envmapStrength', link='_envmapStrength')
|
||||
frag.add_uniform('sampler2D senvmapRadiance', link='_envmapRadiance')
|
||||
frag.add_uniform('sampler2D senvmapBrdf', link='_envmapBrdf')
|
||||
frag.add_uniform('int envmapNumMipmaps', link='_envmapNumMipmaps')
|
||||
frag.write('vec3 n = normalize(wnormal);')
|
||||
frag.write('vec3 l = lightType == 0 ? lightDir : normalize(lightPos - wposition);')
|
||||
frag.write('vec3 v = normalize(eyeDir);')
|
||||
frag.write('vec3 h = normalize(v + l);')
|
||||
frag.write('float dotNL = dot(n, l);')
|
||||
frag.write('float dotNV = dot(n, v);')
|
||||
frag.write('float dotNH = dot(n, h);')
|
||||
frag.write('float dotVH = dot(v, h);')
|
||||
|
||||
vert.add_out('vec4 lampPos')
|
||||
vert.add_uniform('mat4 LWVP', '_lampWorldViewProjectionMatrix')
|
||||
vert.write('lampPos = LWVP * spos;')
|
||||
frag.add_include('../../Shaders/std/shadows.glsl')
|
||||
frag.add_uniform('sampler2D shadowMap', included=True)
|
||||
frag.add_uniform('bool receiveShadow')
|
||||
frag.add_uniform('float shadowsBias', '_lampShadowsBias')
|
||||
frag.write('float visibility = 1.0;')
|
||||
frag.write('if (receiveShadow && lampPos.w > 0.0) {')
|
||||
frag.tab += 1
|
||||
frag.write('vec3 lpos = lampPos.xyz / lampPos.w;')
|
||||
frag.write('lpos.xy = lpos.xy * 0.5 + 0.5;')
|
||||
frag.write('visibility = PCF(lpos.xy, lpos.z - shadowsBias);')
|
||||
frag.tab -= 1
|
||||
frag.write('}')
|
||||
|
||||
frag.add_uniform('float spotlightCutoff', '_spotlampCutoff')
|
||||
frag.add_uniform('float spotlightExponent', '_spotlampExponent')
|
||||
frag.write('if (lightType == 2) {')
|
||||
frag.tab += 1
|
||||
frag.write('float spotEffect = dot(lightDir, l);')
|
||||
frag.write('if (spotEffect < spotlightCutoff) {')
|
||||
frag.tab += 1
|
||||
frag.write('spotEffect = smoothstep(spotlightCutoff - spotlightExponent, spotlightCutoff, spotEffect);')
|
||||
frag.write('visibility *= spotEffect;')
|
||||
frag.tab -= 1
|
||||
frag.write('}')
|
||||
frag.tab -= 1
|
||||
frag.write('}')
|
||||
|
||||
frag.write('vec3 basecol;')
|
||||
frag.write('float roughness;')
|
||||
frag.write('float metallic;')
|
||||
# frag.write('float occlussion;')
|
||||
|
||||
output_node = node_by_type('OUTPUT_MATERIAL')
|
||||
if output_node != None:
|
||||
parse_output(output_node)
|
||||
|
||||
frag.write('vec3 albedo = surfaceAlbedo(basecol, metallic);')
|
||||
frag.write('vec3 f0 = surfaceF0(basecol, metallic);')
|
||||
frag.write('vec3 direct = lambertDiffuseBRDF(albedo, dotNL);')
|
||||
frag.write('direct += specularBRDF(f0, roughness, dotNL, dotNH, dotNV, dotVH);')
|
||||
|
||||
frag.write('vec3 indirect = (shIrradiance(n, 2.2) / PI) * albedo;')
|
||||
frag.write('vec3 reflectionWorld = reflect(-v, n);')
|
||||
frag.write('float lod = getMipFromRoughness(roughness, envmapNumMipmaps);')
|
||||
frag.write('vec3 prefilteredColor = textureLod(senvmapRadiance, envMapEquirect(reflectionWorld), lod).rgb;')
|
||||
frag.write('vec2 envBRDF = texture(senvmapBrdf, vec2(roughness, 1.0 - dotNV)).xy;')
|
||||
frag.write('indirect += prefilteredColor * (f0 * envBRDF.x + envBRDF.y);')
|
||||
|
||||
frag.write('fragColor = vec4(direct * lightColor * visibility + indirect * envmapStrength, 1.0);')
|
||||
|
||||
return con_mesh
|
||||
|
||||
def parse_output(node):
|
||||
out_basecol, out_roughness, out_metallic = parse_shader_input(node.inputs[0])
|
||||
frag.write('basecol = {0};'.format(out_basecol))
|
||||
frag.write('roughness = {0};'.format(out_roughness))
|
||||
frag.write('metallic = {0};'.format(out_metallic))
|
||||
|
||||
def parse_shader_input(inp):
|
||||
if inp.is_linked:
|
||||
return parse_shader(inp.links[0].from_node)
|
||||
else:
|
||||
out_basecol = 'vec3(0.0)'
|
||||
out_roughness = '0.0'
|
||||
out_metallic = '0.0'
|
||||
return out_basecol, out_roughness, out_metallic
|
||||
|
||||
def parse_shader(node):
|
||||
|
||||
if node.type == 'REROUTE':
|
||||
return parse_shader(node.inputs[0].links[0].from_node)
|
||||
|
||||
elif node.type == 'MIX_SHADER':
|
||||
fac = parse_float_input(node.inputs[0])
|
||||
bc1, rough1, met1 = parse_shader_input(node.inputs[1])
|
||||
bc2, rough2, met2 = parse_shader_input(node.inputs[2])
|
||||
out_basecol = '({0} * (1.0 - {2}) + {1} * {2})'.format(bc1, bc2, fac)
|
||||
out_roughness = '({0} * (1.0 - {2}) + {1} * {2})'.format(rough1, rough2, fac)
|
||||
out_metallic = '({0} * (1.0 - {2}) + {1} * {2})'.format(met1, met2, fac)
|
||||
|
||||
elif node.type == 'ADD_SHADER':
|
||||
pass
|
||||
|
||||
elif node.type == 'BSDF_DIFFUSE':
|
||||
out_basecol = parse_color_input(node.inputs[0])
|
||||
out_roughness = parse_float_input(node.inputs[1])
|
||||
out_metallic = '0.0'
|
||||
|
||||
elif node.type == 'BSDF_GLOSSY':
|
||||
out_basecol = parse_color_input(node.inputs[0])
|
||||
out_roughness = parse_float_input(node.inputs[1])
|
||||
out_metallic = '1.0'
|
||||
|
||||
elif node.type == 'AMBIENT_OCCLUSION':
|
||||
pass
|
||||
|
||||
elif node.type == 'BSDF_ANISOTROPIC':
|
||||
pass
|
||||
|
||||
elif node.type == 'EMISSION':
|
||||
pass
|
||||
|
||||
elif node.type == 'BSDF_GLASS':
|
||||
pass
|
||||
|
||||
elif node.type == 'BSDF_HAIR':
|
||||
pass
|
||||
|
||||
elif node.type == 'HOLDOUT':
|
||||
pass
|
||||
|
||||
elif node.type == 'BSDF_REFRACTION':
|
||||
pass
|
||||
|
||||
elif node.type == 'SUBSURFACE_SCATTERING':
|
||||
pass
|
||||
|
||||
elif node.type == 'BSDF_TOON':
|
||||
pass
|
||||
|
||||
elif node.type == 'BSDF_TRANSLUCENT':
|
||||
pass
|
||||
|
||||
elif node.type == 'BSDF_TRANSPARENT':
|
||||
pass
|
||||
|
||||
elif node.type == 'BSDF_VELVET':
|
||||
pass
|
||||
|
||||
elif node.type == 'VOLUME_ABSORPTION':
|
||||
pass
|
||||
|
||||
elif node.type == 'VOLUME_SCATTER':
|
||||
pass
|
||||
|
||||
elif node.type == 'GROUP' and node.node_tree.name.startswith('Armory PBR'):
|
||||
pass
|
||||
|
||||
else:
|
||||
out_basecol = 'vec3(0.0)'
|
||||
out_roughness = '0.0'
|
||||
out_metallic = '0.0'
|
||||
|
||||
return out_basecol, out_roughness, out_metallic
|
||||
|
||||
def parse_color_input(inp):
|
||||
if inp.is_linked:
|
||||
parse_color(inp.links[0].from_node)
|
||||
else:
|
||||
return vec3(inp.default_value)
|
||||
|
||||
def parse_color(node):
|
||||
|
||||
if node.type == 'REROUTE':
|
||||
return parse_color(node.inputs[0].links[0].from_node)
|
||||
|
||||
elif node.type == 'ATTRIBUTE':
|
||||
pass
|
||||
|
||||
elif node.type == 'RGB':
|
||||
pass
|
||||
|
||||
elif node.type == 'TEX_BRICK':
|
||||
pass
|
||||
|
||||
elif node.type == 'TEX_CHECKER':
|
||||
pass
|
||||
|
||||
elif node.type == 'TEX_ENVIRONMENT':
|
||||
pass
|
||||
|
||||
elif node.type == 'TEX_GRADIENT':
|
||||
pass
|
||||
|
||||
elif node.type == 'TEX_IMAGE':
|
||||
pass
|
||||
|
||||
elif node.type == 'TEX_MAGIC':
|
||||
pass
|
||||
|
||||
elif node.type == 'TEX_MUSGRAVE':
|
||||
pass
|
||||
|
||||
elif node.type == 'TEX_NOISE':
|
||||
pass
|
||||
|
||||
elif node.type == 'TEX_POINTDENSITY':
|
||||
pass
|
||||
|
||||
elif node.type == 'TEX_SKY':
|
||||
pass
|
||||
|
||||
elif node.type == 'TEX_VORONOI':
|
||||
pass
|
||||
|
||||
elif node.type == 'TEX_WAVE':
|
||||
pass
|
||||
|
||||
elif node.type == 'BRIGHTCONTRAST':
|
||||
pass
|
||||
|
||||
elif node.type == 'GAMMA':
|
||||
pass
|
||||
|
||||
elif node.type == 'HUE_SAT':
|
||||
pass
|
||||
|
||||
elif node.type == 'INVERT':
|
||||
pass
|
||||
|
||||
elif node.type == 'MIX_RGB':
|
||||
pass
|
||||
|
||||
elif node.type == 'CURVE_RGB':
|
||||
pass
|
||||
|
||||
elif node.type == 'BLACKBODY':
|
||||
pass
|
||||
|
||||
elif node.type == 'VALTORGB':
|
||||
pass
|
||||
|
||||
elif node.type == 'COMBHSV':
|
||||
pass
|
||||
|
||||
elif node.type == 'COMBRGB':
|
||||
pass
|
||||
|
||||
elif node.type == 'WAVELENGTH':
|
||||
pass
|
||||
|
||||
def parse_vector_input(inp):
|
||||
if inp.is_linked:
|
||||
return parse_vector(inp.links[0].from_node)
|
||||
else:
|
||||
return vec3(inp.default_value)
|
||||
|
||||
def parse_vector(node):
|
||||
if node.type == 'REROUTE':
|
||||
return parse_vector(node.inputs[0].links[0].from_node)
|
||||
|
||||
elif node.type == 'ATTRIBUTE':
|
||||
pass
|
||||
|
||||
elif node.type == 'CAMERA':
|
||||
pass
|
||||
|
||||
elif node.type == 'NEW_GEOMETRY':
|
||||
pass
|
||||
|
||||
elif node.type == 'HAIR_INFO':
|
||||
pass
|
||||
|
||||
elif node.type == 'OBJECT_INFO':
|
||||
pass
|
||||
|
||||
elif node.type == 'PARTICLE_INFO':
|
||||
pass
|
||||
|
||||
elif node.type == 'TANGENT':
|
||||
pass
|
||||
|
||||
elif node.type == 'TEX_COORD':
|
||||
pass
|
||||
|
||||
elif node.type == 'UVMAP':
|
||||
pass
|
||||
|
||||
elif node.type == 'BUMP':
|
||||
pass
|
||||
|
||||
elif node.type == 'MAPPING':
|
||||
pass
|
||||
|
||||
elif node.type == 'NORMAL':
|
||||
pass
|
||||
|
||||
elif node.type == 'NORMAL_MAP':
|
||||
pass
|
||||
|
||||
elif node.type == 'CURVE_VEC':
|
||||
pass
|
||||
|
||||
elif node.type == 'VECT_TRANSFORM':
|
||||
pass
|
||||
|
||||
elif node.type == 'COMBXYZ':
|
||||
pass
|
||||
|
||||
elif node.type == 'VECT_MATH':
|
||||
pass
|
||||
|
||||
def parse_float_input(inp):
|
||||
if inp.is_linked:
|
||||
return parse_float(inp.links[0].from_node)
|
||||
else:
|
||||
return vec1(inp.default_value)
|
||||
|
||||
def parse_float(node):
|
||||
if node.type == 'REROUTE':
|
||||
return parse_float(node.inputs[0].links[0].from_node)
|
||||
|
||||
if node.type == 'ATTRIBUTE':
|
||||
pass
|
||||
|
||||
elif node.type == 'CAMERA':
|
||||
pass
|
||||
|
||||
elif node.type == 'FRESNEL':
|
||||
pass
|
||||
|
||||
elif node.type == 'NEW_GEOMETRY':
|
||||
pass
|
||||
|
||||
elif node.type == 'HAIR_INFO':
|
||||
pass
|
||||
|
||||
elif node.type == 'LAYER_WEIGHT':
|
||||
pass
|
||||
|
||||
elif node.type == 'LIGHT_PATH':
|
||||
pass
|
||||
|
||||
elif node.type == 'OBJECT_INFO':
|
||||
pass
|
||||
|
||||
elif node.type == 'PARTICLE_INFO':
|
||||
pass
|
||||
|
||||
elif node.type == 'VALUE':
|
||||
return vec1(node.outputs[0].default_value)
|
||||
|
||||
elif node.type == 'WIREFRAME':
|
||||
pass
|
||||
|
||||
elif node.type == 'TEX_BRICK':
|
||||
pass
|
||||
|
||||
elif node.type == 'TEX_CHECKER':
|
||||
pass
|
||||
|
||||
elif node.type == 'TEX_GRADIENT':
|
||||
pass
|
||||
|
||||
elif node.type == 'TEX_IMAGE':
|
||||
pass
|
||||
|
||||
elif node.type == 'TEX_MAGIC':
|
||||
pass
|
||||
|
||||
elif node.type == 'TEX_MUSGRAVE':
|
||||
pass
|
||||
|
||||
elif node.type == 'TEX_NOISE':
|
||||
pass
|
||||
|
||||
elif node.type == 'TEX_POINTDENSITY':
|
||||
pass
|
||||
|
||||
elif node.type == 'TEX_VORONOI':
|
||||
pass
|
||||
|
||||
elif node.type == 'TEX_WAVE':
|
||||
pass
|
||||
|
||||
elif node.type == 'LIGHT_FALLOFF':
|
||||
pass
|
||||
|
||||
elif node.type == 'NORMAL':
|
||||
pass
|
||||
|
||||
elif node.type == 'VALTORGB':
|
||||
pass
|
||||
|
||||
elif node.type == 'MATH':
|
||||
pass
|
||||
|
||||
elif node.type == 'RGBTOBW':
|
||||
pass
|
||||
|
||||
elif node.type == 'SEPHSV':
|
||||
pass
|
||||
|
||||
elif node.type == 'SEPRGB':
|
||||
pass
|
||||
|
||||
elif node.type == 'SEPXYZ':
|
||||
pass
|
||||
|
||||
elif node.type == 'VECT_MATH':
|
||||
pass
|
||||
|
||||
def vec1(v):
|
||||
return str(v)
|
||||
|
||||
def vec2(v):
|
||||
return 'vec2({0}, {1})'.format(v[0], v[1])
|
||||
|
||||
def vec3(v):
|
||||
return 'vec3({0}, {1}, {2})'.format(v[0], v[1], v[2])
|
||||
|
||||
def vec4(v):
|
||||
return 'vec4({0}, {1}, {2}, {3})'.format(v[0], v[1], v[2], v[3])
|
||||
|
||||
def shadows(context_id):
|
||||
con_shadowmap = state.data.add_context({ 'name': context_id, 'depth_write': True, 'compare_mode': 'less', 'cull_mode': 'clockwise' })
|
||||
|
||||
vert = con_shadowmap.vert()
|
||||
vert.add_uniform('mat4 LWVP', '_lampWorldViewProjectionMatrix')
|
||||
vert.write('gl_Position = LWVP * vec4(pos, 1.0);')
|
||||
|
||||
frag = con_shadowmap.frag()
|
||||
frag.write('fragColor = vec4(0.0);')
|
||||
|
||||
return con_shadowmap
|
||||
|
51
blender/material/shader.py
Normal file
51
blender/material/shader.py
Normal file
|
@ -0,0 +1,51 @@
|
|||
|
||||
class Shader:
|
||||
|
||||
def __init__(self, context):
|
||||
self.context = context
|
||||
self.includes = []
|
||||
self.ins = []
|
||||
self.outs = []
|
||||
self.uniforms = []
|
||||
self.main = ''
|
||||
self.tab = 1
|
||||
|
||||
def add_include(self, s):
|
||||
self.includes.append(s)
|
||||
|
||||
def add_in(self, s):
|
||||
self.ins.append(s)
|
||||
|
||||
def add_out(self, s):
|
||||
self.outs.append(s)
|
||||
|
||||
def add_uniform(self, s, link=None, included=False):
|
||||
ar = s.split(' ')
|
||||
if ar[0] == 'sampler2D':
|
||||
self.context.add_texture_unit(ar[0], ar[1], link=link)
|
||||
else:
|
||||
if ar[0] == 'float' and '[' in ar[1]:
|
||||
ar[0] = 'floats'
|
||||
ar[1] = ar[1].split('[', 1)[0]
|
||||
self.context.add_constant(ar[0], ar[1], link=link)
|
||||
if included == False:
|
||||
self.uniforms.append(s)
|
||||
|
||||
def write(self, s):
|
||||
self.main += '\t' * self.tab + s + '\n'
|
||||
|
||||
def get(self):
|
||||
s = '#version 450\n'
|
||||
for a in self.includes:
|
||||
s += '#include "' + a + '"\n'
|
||||
for a in self.ins:
|
||||
s += 'in ' + a + ';\n'
|
||||
for a in self.outs:
|
||||
s += 'out ' + a + ';\n'
|
||||
for a in self.uniforms:
|
||||
s += 'uniform ' + a + ';\n'
|
||||
|
||||
s += 'void main() {\n'
|
||||
s += self.main
|
||||
s += '}\n'
|
||||
return s
|
101
blender/material/shader_data.py
Normal file
101
blender/material/shader_data.py
Normal file
|
@ -0,0 +1,101 @@
|
|||
from material.shader import Shader
|
||||
|
||||
class ShaderData:
|
||||
|
||||
def __init__(self, material):
|
||||
self.material = material
|
||||
self.contexts = []
|
||||
|
||||
self.sd = {}
|
||||
self.data = {}
|
||||
self.data['shader_datas'] = [self.sd]
|
||||
|
||||
self.sd['name'] = material.name + '_data'
|
||||
|
||||
self.sd['vertex_structure'] = []
|
||||
self.add_elem('pos', 3)
|
||||
self.add_elem('nor', 3)
|
||||
|
||||
self.sd['contexts'] = []
|
||||
|
||||
def add_elem(self, name, size):
|
||||
elem = { 'name': name, 'size': size }
|
||||
self.sd['vertex_structure'].append(elem)
|
||||
|
||||
def add_context(self, props):
|
||||
con = ShaderContext(self.material, self.sd, props)
|
||||
self.sd['contexts'].append(con.get())
|
||||
return con
|
||||
|
||||
def get(self):
|
||||
return self.data
|
||||
|
||||
class ShaderContext:
|
||||
|
||||
def __init__(self, material, shader_data, props):
|
||||
self.material = material
|
||||
self.shader_data = shader_data
|
||||
self.data = {}
|
||||
self.data['name'] = props['name']
|
||||
self.data['depth_write'] = props['depth_write']
|
||||
self.data['compare_mode'] = props['compare_mode']
|
||||
self.data['cull_mode'] = props['cull_mode']
|
||||
|
||||
self.data['texture_units'] = []
|
||||
self.tunits = self.data['texture_units']
|
||||
self.data['constants'] = []
|
||||
self.constants = self.data['constants']
|
||||
|
||||
def get(self):
|
||||
return self.data
|
||||
|
||||
def add_constant(self, ctype, name, link=None):
|
||||
for c in self.constants:
|
||||
if c['name'] == name:
|
||||
return
|
||||
|
||||
c = { 'name': name, 'type': ctype }
|
||||
if link != None:
|
||||
c['link'] = link
|
||||
self.constants.append(c)
|
||||
|
||||
def add_texture_unit(self, ctype, name, link=None):
|
||||
for c in self.tunits:
|
||||
if c['name'] == name:
|
||||
return
|
||||
|
||||
c = { 'name': name }
|
||||
if link != None:
|
||||
c['link'] = link
|
||||
self.tunits.append(c)
|
||||
|
||||
def vert(self):
|
||||
self.data['vertex_shader'] = self.material.name + '_' + self.data['name'] + '.vert'
|
||||
self.vert = Shader(self)
|
||||
|
||||
vs = self.shader_data['vertex_structure']
|
||||
for e in vs:
|
||||
self.vert.add_in('vec' + str(e['size']) + ' ' + e['name'])
|
||||
return self.vert
|
||||
|
||||
def frag(self):
|
||||
self.data['fragment_shader'] = self.material.name + '_' + self.data['name'] + '.frag'
|
||||
self.frag = Shader(self)
|
||||
self.frag.ins = self.vert.outs
|
||||
self.frag.add_out('vec4 fragColor')
|
||||
return self.frag
|
||||
|
||||
def geom(self):
|
||||
self.data['geometry_shader'] = self.material.name + '_' + self.data['name'] + '.geom'
|
||||
self.geom = Shader(self)
|
||||
return self.geom
|
||||
|
||||
def tesc(self):
|
||||
self.data['tesscontrol_shader'] = self.material.name + '_' + self.data['name'] + '.tesc'
|
||||
self.tesc = Shader(self)
|
||||
return self.tesc
|
||||
|
||||
def tese(self):
|
||||
self.data['tesseval_shader'] = self.material.name + '_' + self.data['name'] + '.tese'
|
||||
self.tese = Shader(self)
|
||||
return self.tese
|
8
blender/material/state.py
Normal file
8
blender/material/state.py
Normal file
|
@ -0,0 +1,8 @@
|
|||
data = None
|
||||
material = None
|
||||
group = None
|
||||
nodes = None
|
||||
links = None
|
||||
mat_context = None
|
||||
defs = None
|
||||
path = None
|
|
@ -74,6 +74,10 @@ def init_properties():
|
|||
bpy.types.World.arm_lod_advanced = BoolProperty(name="Advanced", default=False)
|
||||
bpy.types.World.arm_lod_gen_levels = IntProperty(name="Levels", description="Number of levels to generate", default=3, min=1)
|
||||
bpy.types.World.arm_lod_gen_ratio = FloatProperty(name="Decimate Ratio", description="Decimate ratio", default=0.8)
|
||||
bpy.types.World.arm_material_level = EnumProperty(
|
||||
items=[('Restricted', 'Restricted', 'Restricted'),
|
||||
('Full', 'Full', 'Full')],
|
||||
name="Materials", description="Node parser to use when building materials", default="Restricted")
|
||||
bpy.types.World.arm_cache_shaders = BoolProperty(name="Cache Shaders", description="Do not rebuild existing shaders", default=True, update=assets.invalidate_shader_cache)
|
||||
#bpy.types.World.arm_cache_envmaps = BoolProperty(name="Cache Envmaps", description="Do not remove prefiltered maps when cleaning project", default=True)
|
||||
bpy.types.World.arm_play_live_patch = BoolProperty(name="Live Patching", description="Sync running player data to Blender", default=True)
|
||||
|
|
|
@ -383,6 +383,7 @@ class ArmoryBuildPanel(bpy.types.Panel):
|
|||
layout.prop(wrd, 'arm_optimize_mesh')
|
||||
layout.prop(wrd, 'arm_sampled_animation')
|
||||
layout.prop(wrd, 'arm_deinterleaved_buffers')
|
||||
layout.prop(wrd, 'arm_material_level')
|
||||
layout.label('Libraries')
|
||||
layout.prop(wrd, 'arm_physics')
|
||||
layout.prop(wrd, 'arm_navigation')
|
||||
|
|
Loading…
Reference in a new issue