Pixel-Composer/scripts/BBMOD_Material/BBMOD_Material.gml
2023-04-23 16:47:33 +02:00

770 lines
19 KiB
Text

/// @func __bbmod_material_get_map()
///
/// @desc Retrieves a map of registered materials.
///
/// @return {Id.DsMap<String, Struct.BBMOD_Material>} The map of registered
/// materials.
///
/// @private
function __bbmod_material_get_map()
{
static _map = ds_map_create();
return _map;
}
/// @func bbmod_material_register(_name, _material)
///
/// @desc Registers a material.
///
/// @param {String} _name The name of the material.
/// @param {Struct.BBMOD_Material} _material The material.
function bbmod_material_register(_name, _material)
{
gml_pragma("forceinline");
static _map =__bbmod_material_get_map();
_map[? _name] = _material;
_material.__name = _name;
}
/// @func bbmod_material_exists(_name)
///
/// @desc Checks if there is a material registered under the name.
///
/// @param {String} _name The name of the material.
///
/// @return {Bool} Returns `true` if there is a material registered under the
/// name.
function bbmod_material_exists(_name)
{
gml_pragma("forceinline");
static _map =__bbmod_material_get_map();
return ds_map_exists(_map, _name);
}
/// @func bbmod_material_get(_name)
///
/// @desc Retrieves a material registered under the name.
///
/// @param {String} _name The name of the material.
///
/// @return {Struct.BBMOD_Material} The material or `undefined` if no
/// material registered under the given name exists.
function bbmod_material_get(_name)
{
gml_pragma("forceinline");
static _map =__bbmod_material_get_map();
return _map[? _name];
}
/// @var {Struct.BBMOD_Material} The currently applied material or `undefined`.
/// @private
global.__bbmodMaterialCurrent = undefined;
/// @func BBMOD_Material([_shader])
///
/// @extends BBMOD_Resource
///
/// @desc Base class for materials.
///
/// @param {Struct.BBMOD_Shader} [_shader] A shader that the material uses in
/// the {@link BBMOD_ERenderPass.Forward} pass. Leave `undefined` if you would
/// like to use {@link BBMOD_Material.set_shader} to specify shaders used in
/// specific render passes.
///
/// @see BBMOD_Shader
function BBMOD_Material(_shader=undefined)
: BBMOD_Resource() constructor
{
BBMOD_CLASS_GENERATED_BODY;
static Resource_destroy = destroy;
/// @var {String} The name under which is this material registered or
/// `undefined`.
/// @private
__name = undefined;
/// @var {Real} Render passes in which is the material rendered. Defaults
/// to 0 (no passes).
/// @readonly
/// @see BBMOD_ERenderPass
RenderPass = 0;
/// @var {Array<Struct.BBMOD_Shader>} Shaders used in specific render passes.
/// @private
/// @see BBMOD_Material.set_shader
/// @see BBMOD_Material.get_shader
Shaders = array_create(BBMOD_ERenderPass.SIZE, undefined);
/// @var {Struct.BBMOD_RenderQueue} The render queue used by this material.
/// Defaults to the default BBMOD render queue.
/// @readonly
/// @see BBMOD_RenderQueue
/// @see bbmod_render_queue_get_default
RenderQueue = bbmod_render_queue_get_default();
/// @var {Function} A function that is executed when the shader is applied.
/// Must take the material as the first argument. Use `undefined` if you do
/// not want to execute any function. Defaults to `undefined`.
OnApply = undefined;
/// @var {Constant.BlendMode} A blend mode. Default value is `bm_normal`.
BlendMode = bm_normal;
/// @var {Constant.CullMode} A culling mode. Default value is
/// `cull_counterclockwise`.
Culling = cull_counterclockwise;
/// @var {Bool} If `true` then models using this material should write to
/// the depth buffer. Default value is `true`.
ZWrite = true;
/// @var {Bool} If `true` then models using this material should be tested
/// against the depth buffer. Defaults value is `true`.
ZTest = true;
/// @var {Constant.CmpFunc} The function used for depth testing when
/// {@link BBMOD_Material.ZTest} is enabled. Default value is
/// `cmpfunc_lessequal`.
ZFunc = cmpfunc_lessequal;
/// @var {Real} Discard pixels with alpha less than this value. Use values
/// in range 0..1.
AlphaTest = 1.0;
/// @var {Bool} Use `true` to enable alpha blending. This can have negative
/// effect on performance, therefore it should be used only when necessary.
/// Default value is `false`.
AlphaBlend = false;
/// @var {Real} Use one of the `mip_` constants. Default value is `mip_on`.
Mipmapping = mip_on;
/// @var {Real} Defines a bias for which mip level is used. Can be also
/// negative values to select lower mip levels. E.g. if mip level 2 would be
/// normally selected and bias was -1, then level 1 would be selected instead
/// and if it was 1, then level 3 would be selected instead. Default value is
/// 0.
MipBias = 0;
/// @var {Real} The mip filter mode used for the material. Use one of the
/// `tf_` constants. Default value is `tf_anisotropic`.
MipFilter = tf_anisotropic;
/// @var {Real} The minimum mip level used, where 0 is the highest resolution,
/// 1 is the first mipmap, 2 is the second etc. Default value is 0.
MipMin = 0;
/// @var {Real} The maximum mip level used, where 0 is the highest resolution,
/// 1 is the first mipmap, 2 is the second etc. Default value is 16.
MipMax = 16;
/// @var {Real} The maximum level of anisotropy when
/// {@link BBMOD_Material.MipFilter} is set to `tf_anisotropic`. Must be in
/// range 1..16. Default value is 16.
Anisotropy = 16;
/// @var {Bool} Use `false` to disable linear texture filtering for this
/// material. Default value is `true`.
Filtering = true;
/// @var {Bool} Use `true` to enable texture repeat for this material.
/// Default value is `false`.
Repeat = false;
/// @var {Pointer.Texture} A texture with a base color in the RGB channels
/// and opacity in the alpha channel.
BaseOpacity = pointer_null;
__baseOpacitySprite = undefined;
/// @func copy(_dest)
///
/// @desc Copies properties of this material into another material.
///
/// @param {Struct.BBMOD_Material} _dest The destination material.
///
/// @return {Struct.BBMOD_Material} Returns `self`.
static copy = function (_dest) {
_dest.__name = __name;
_dest.RenderPass = RenderPass;
_dest.Shaders = array_create(BBMOD_ERenderPass.SIZE, undefined);
array_copy(_dest.Shaders, 0, Shaders, 0, BBMOD_ERenderPass.SIZE);
_dest.RenderQueue = RenderQueue;
_dest.OnApply = OnApply;
_dest.BlendMode = BlendMode;
_dest.Culling = Culling;
_dest.ZWrite = ZWrite;
_dest.ZTest = ZTest;
_dest.ZFunc = ZFunc;
_dest.AlphaTest = AlphaTest;
_dest.AlphaBlend = AlphaBlend;
_dest.Mipmapping = Mipmapping;
_dest.MipBias = MipBias;
_dest.MipFilter = MipFilter;
_dest.MipMin = MipMin;
_dest.MipMax = MipMax;
_dest.Anisotropy = Anisotropy;
_dest.Filtering = Filtering;
_dest.Repeat = Repeat;
if (_dest.__baseOpacitySprite != undefined)
{
sprite_delete(_dest.__baseOpacitySprite);
_dest.__baseOpacitySprite = undefined;
}
if (__baseOpacitySprite != undefined)
{
_dest.__baseOpacitySprite = sprite_duplicate(__baseOpacitySprite);
_dest.BaseOpacity = sprite_get_texture(_dest.__baseOpacitySprite, 0);
}
else
{
_dest.BaseOpacity = BaseOpacity;
}
return self;
};
/// @func clone()
///
/// @desc Creates a clone of the material.
///
/// @return {Struct.BBMOD_Material} The created clone.
static clone = function () {
var _clone = new BBMOD_Material();
copy(_clone);
return _clone;
};
/// @func to_json(_json)
///
/// @desc Saves material properties to a JSON object.
///
/// @param {Struct} _json The object to save the properties to.
///
/// @return {Struct.BBMOD_Material} Returns `self`.
///
/// @throws {BBMOD_Exception} If an error occurs.
static to_json = function (_json) {
var _shaders = {};
var _pass = 0;
repeat (BBMOD_ERenderPass.SIZE)
{
var _shader = Shaders[_pass];
if (_shader != undefined)
{
var _passName = bbmod_render_pass_to_string(_pass);
if (_shader.__name == undefined)
{
throw new BBMOD_Exception(
"Cannot save to JSON, shader for render pass \""
+ _passName + "\" is not registered!");
}
else
{
_shaders[$ _passName] = _shader.__name;
}
}
++_pass;
}
_json.Shaders = _shaders;
if (RenderQueue.Name != undefined)
{
_json.RenderQueue = RenderQueue.Name;
}
// TODO: Save OnApply
_json.BlendMode = BlendMode;
_json.Culling = Culling;
_json.ZWrite = ZWrite;
_json.ZTest = ZTest;
_json.ZFunc = ZFunc;
_json.AlphaTest = AlphaTest;
_json.AlphaBlend = AlphaBlend;
_json.Mipmapping = Mipmapping;
_json.MipBias = MipBias;
_json.MipFilter = MipFilter;
_json.MipMin = MipMin;
_json.MipMax = MipMax;
_json.Anisotropy = Anisotropy;
_json.Filtering = Filtering;
_json.Repeat = Repeat;
// TODO: Save BaseOpacity/__baseOpacitySprite
return self;
};
/// @func from_json(_json)
///
/// @desc Loads material properties from a JSON object.
///
/// @param {Struct} _json The object to load the properties from.
///
/// @return {Struct.BBMOD_Material} Returns `self`.
///
/// @throws {BBMOD_Exception} If an error occurs.
static from_json = function (_json) {
if (variable_struct_exists(_json, "Shaders"))
{
var _shaders = _json.Shaders;
var _keys = variable_struct_get_names(_shaders);
var _index = 0;
repeat (array_length(_keys))
{
var _passName = _keys[_index++];
var _pass = bbmod_render_pass_from_string(_passName);
var _shader = _shaders[$ _passName];
if (is_string(_shader))
{
_shader = bbmod_shader_get(_shader);
}
set_shader(_pass, _shader);
}
}
if (variable_struct_exists(_json, "RenderQueue"))
{
var _renderQueue = _json.RenderQueue;
if (is_string(_renderQueue))
{
var _renderQueues = bbmod_render_queues_get();
var _index = 0;
repeat (array_length(_renderQueues))
{
with (_renderQueues[_index++])
{
if (Name == _renderQueue)
{
_renderQueue = self;
break;
}
}
}
if (is_string(_renderQueue))
{
throw new BBMOD_Exception("Invalid render queue \"" + _renderQueue + "\"!");
}
}
}
if (variable_struct_exists(_json, "OnApply"))
{
OnApply = _json.OnApply;
}
if (variable_struct_exists(_json, "BlendMode"))
{
BlendMode = _json.BlendMode;
}
if (variable_struct_exists(_json, "Culling"))
{
Culling = _json.Culling;
}
if (variable_struct_exists(_json, "ZWrite"))
{
ZWrite = _json.ZWrite;
}
if (variable_struct_exists(_json, "ZTest"))
{
ZTest = _json.ZTest;
}
if (variable_struct_exists(_json, "ZFunc"))
{
ZFunc = _json.ZFunc;
}
if (variable_struct_exists(_json, "AlphaTest"))
{
AlphaTest = _json.AlphaTest;
}
if (variable_struct_exists(_json, "AlphaBlend"))
{
AlphaBlend = _json.AlphaBlend;
}
if (variable_struct_exists(_json, "Mipmapping"))
{
Mipmapping = _json.Mipmapping;
}
if (variable_struct_exists(_json, "MipBias"))
{
MipBias = _json.MipBias;
}
if (variable_struct_exists(_json, "MipFilter"))
{
MipFilter = _json.MipFilter;
}
if (variable_struct_exists(_json, "MipMin"))
{
MipMin = _json.MipMin;
}
if (variable_struct_exists(_json, "MipMax"))
{
MipMax = _json.MipMax;
}
if (variable_struct_exists(_json, "Anisotropy"))
{
Anisotropy = _json.Anisotropy;
}
if (variable_struct_exists(_json, "Filtering"))
{
Filtering = _json.Filtering;
}
if (variable_struct_exists(_json, "Repeat"))
{
Repeat = _json.Repeat;
}
if (variable_struct_exists(_json, "BaseOpacity"))
{
if (__baseOpacitySprite != undefined)
{
sprite_delete(__baseOpacitySprite);
__baseOpacitySprite = undefined;
}
BaseOpacity = _json.BaseOpacity;
}
return self;
};
static to_file = function (_file) {
var _dirname = filename_dir(_file);
if (!directory_exists(_dirname))
{
directory_create(_dirname);
}
var _json = {};
to_json(_json);
var _jsonFile = file_text_open_write(_file);
file_text_write_string(_jsonFile, json_stringify(_json));
file_text_close(_jsonFile);
return self;
};
static from_file = function (_file, _sha1=undefined) {
Path = _file;
check_file(_file, _sha1);
from_json(bbmod_json_load(_file));
IsLoaded = true;
return self;
};
static from_file_async = function (_file, _sha1=undefined, _callback=undefined) {
Path = _file;
if (!check_file(_file, _sha1, _callback ?? bbmod_empty_callback))
{
return self;
}
var _json;
try
{
_json = bbmod_json_load(_file);
}
catch (_err)
{
if (_callback)
{
_callback(_err, self);
}
return self;
}
from_json(_json);
IsLoaded = true;
if (_callback != undefined)
{
_callback(undefined, self);
}
return self;
};
static _make_sprite = function (_r, _g, _b, _a) {
gml_pragma("forceinline");
static _sur = noone;
if (!surface_exists(_sur))
{
_sur = surface_create(1, 1);
}
surface_set_target(_sur);
draw_clear_alpha(make_color_rgb(_r, _g, _b), _a);
surface_reset_target();
return sprite_create_from_surface(_sur, 0, 0, 1, 1, false, false, 0, 0);
};
/// @func set_base_opacity(_color)
///
/// @desc Changes the base color and opacity to a uniform value for the
/// entire material.
///
/// @param {Struct.BBMOD_Color} _color The new base color and opacity.
///
/// @return {Struct.BBMOD_BaseMaterial} Returns `self`.
static set_base_opacity = function (_color) {
if (__baseOpacitySprite != undefined)
{
sprite_delete(__baseOpacitySprite);
}
var _isReal = is_real(_color);
__baseOpacitySprite = _make_sprite(
_isReal ? color_get_red(_color) : _color.Red,
_isReal ? color_get_green(_color) : _color.Green,
_isReal ? color_get_blue(_color) : _color.Blue,
_isReal ? argument[1] : _color.Alpha
);
BaseOpacity = sprite_get_texture(__baseOpacitySprite, 0);
return self;
};
/// @func apply(_vertexFormat)
///
/// @desc Makes this material the current one.
///
/// @param {Struct.BBMOD_VertexFormat} _vertexFormat The vertex format of
/// meshes that we are going to use the material for.
///
/// @return {Bool} Returns `true` if the material was applied.
///
/// @see BBMOD_Material.reset
static apply = function (_vertexFormat) {
if ((RenderPass & (1 << bbmod_render_pass_get())) == 0)
{
return false;
}
var _shader = Shaders[bbmod_render_pass_get()];
var _shaderRaw = _shader.get_variant(_vertexFormat);
if (_shaderRaw == undefined)
{
__bbmod_warning(
"Shader variant for vertex format "
+ string(_vertexFormat.get_hash())
+ " was not found! Material not applied!");
return false;
}
var _shaderChanged = false;
if (BBMOD_SHADER_CURRENT != _shader
|| shader_current() != _shaderRaw)
{
if (BBMOD_SHADER_CURRENT != undefined)
{
BBMOD_SHADER_CURRENT.reset();
}
shader_set(_shaderRaw);
BBMOD_SHADER_CURRENT = _shader;
_shaderChanged = true;
}
if (global.__bbmodMaterialCurrent != self)
{
// TODO: GPU settings override per render pass!
var _isShadows = (bbmod_render_pass_get() == BBMOD_ERenderPass.Shadows);
if (global.__bbmodMaterialCurrent != undefined)
{
gpu_pop_state();
}
gpu_push_state();
if (_shaderChanged)
{
with (_shader)
{
on_set();
__bbmod_shader_set_globals(_shaderRaw);
}
_shaderChanged = false;
}
gpu_set_blendmode(_isShadows ? bm_normal : BlendMode);
gpu_set_blendenable(_isShadows ? false : AlphaBlend);
gpu_set_cullmode(Culling);
gpu_set_zwriteenable(_isShadows ? true : ZWrite);
gpu_set_ztestenable(_isShadows ? true : ZTest);
gpu_set_zfunc(ZFunc);
gpu_set_tex_mip_enable(Mipmapping);
gpu_set_tex_mip_bias(MipBias);
gpu_set_tex_mip_filter(MipFilter);
gpu_set_tex_min_mip(MipMin);
gpu_set_tex_max_mip(MipMax);
gpu_set_tex_max_aniso(Anisotropy);
gpu_set_tex_filter(Filtering);
gpu_set_tex_repeat(Repeat);
_shader.set_material(self);
global.__bbmodMaterialCurrent = self;
}
if (_shaderChanged)
{
with (_shader)
{
on_set();
__bbmod_shader_set_globals(_shaderRaw);
}
_shader.set_material(self);
}
if (OnApply != undefined)
{
OnApply(self);
}
return true;
};
/// @func set_shader(_pass, _shader)
///
/// @desc Defines a shader used in a specific render pass.
///
/// @param {Real} _pass The render pass. Use values from {@link BBMOD_ERenderPass}.
/// @param {Struct.BBMOD_Shader} _shader The shader used in the render pass.
///
/// @return {Struct.BBMOD_Material} Returns `self`.
///
/// @see BBMOD_Material.get_shader
/// @see bbmod_render_pass_set
static set_shader = function (_pass, _shader) {
gml_pragma("forceinline");
RenderPass |= (1 << _pass);
Shaders[@ _pass] = _shader;
return self;
};
/// @func has_shader(_pass)
///
/// @desc Checks whether the material has a shader for the render pass.
///
/// @param {Real} _pass The render pass. Use values from {@link BBMOD_ERenderPass}.
///
/// @return {Bool} Returns `true` if the material has a shader for the
/// render pass.
static has_shader = function (_pass) {
gml_pragma("forceinline");
return ((RenderPass & (1 << _pass)) != 0);
};
/// @func get_shader(_pass)
///
/// @desc Retrieves a shader used in a specific render pass.
///
/// @param {Real} _pass The render pass. Use values from
/// {@link BBMOD_ERenderPass}.
///
/// @return {Struct.BBMOD_Shader} The shader or `undefined`.
///
/// @see BBMOD_Material.set_shader
static get_shader = function (_pass) {
gml_pragma("forceinline");
return Shaders[_pass];
};
/// @func remove_shader(_pass)
///
/// @desc Removes a shader used in a specific render pass.
///
/// @param {Real} _pass The render pass.
///
/// @return {Struct.BBMOD_Material} Returns `self`.
static remove_shader = function (_pass) {
gml_pragma("forceinline");
RenderPass &= ~(1 << _pass);
Shaders[@ _pass] = undefined;
return self;
};
/// @func reset()
///
/// @desc Resets the current material to `undefined`.
///
/// @return {Struct.BBMOD_Material} Returns `self`.
///
/// @see BBMOD_Material.apply
/// @see bbmod_material_reset
static reset = function () {
gml_pragma("forceinline");
bbmod_material_reset();
return self;
};
static destroy = function () {
Resource_destroy();
if (__baseOpacitySprite != undefined)
{
sprite_delete(__baseOpacitySprite);
}
return undefined;
};
if (_shader != undefined)
{
set_shader(BBMOD_ERenderPass.Forward, _shader);
}
}
/// @func bbmod_material_reset()
///
/// @desc Resets the current material to `undefined`. Every block of code
/// rendering models must start and end with this function!
///
/// @example
/// ```gml
/// bbmod_material_reset();
///
/// // Render static batch of trees
/// treeBatch.submit(matTree);
///
/// // Render characters
/// var _world = matrix_get(matrix_world);
/// with (OCharacter)
/// {
/// matrix_set(matrix_world, matrix_build(x, y, z, 0, 0, direction, 1, 1, 1));
/// animationPlayer.submit();
/// }
/// matrix_set(matrix_world, _world);
///
/// bbmod_material_reset();
/// ```
/// @see BBMOD_Material.reset
function bbmod_material_reset()
{
gml_pragma("forceinline");
if (global.__bbmodMaterialCurrent != undefined)
{
gpu_pop_state();
global.__bbmodMaterialCurrent = undefined;
}
if (BBMOD_SHADER_CURRENT != undefined)
{
BBMOD_SHADER_CURRENT.reset();
}
}