/// @macro {Real} Maximum number of vec4 uniforms for dynamic batch data /// available in the default shaders. Equals to 192. #macro BBMOD_MAX_BATCH_VEC4S 192 /// @func BBMOD_DynamicBatch([_model[, _size[, _slotsPerInstance]]]) /// /// @extends BBMOD_Class /// /// @desc A dynamic batch is a structure that allows you to render multiple /// instances of a single model at once, each with its own position, scale and /// rotation. Compared to {@link BBMOD_Model.submit}, this drastically reduces /// draw calls and increases performance, but requires more memory. Number of /// model instances per batch is also affected by maximum number of uniforms /// that a vertex shader can accept. /// /// @param {Struct.BBMOD_Model} [_model] The model to create a dynamic batch of. /// @param {Real} [_size] Number of model instances in the batch. Default value /// is 32. /// @param {Real} [_slotsPerInstance] Number of slots that each instance takes /// in the data array. Default value is 12. /// /// @example /// Following code renders all instances of a car object in batches of 64. /// ```gml /// /// @desc Create event /// modCar = new BBMOD_Model("Car.bbmod"); /// matCar = new BBMOD_DefaultMaterial(BBMOD_ShDefaultBatched, /// sprite_get_texture(SprCar, 0)); /// carBatch = new BBMOD_DynamicBatch(modCar, 64); /// /// /// @desc Draw event /// carBatch.render_object(OCar, matCar); /// ``` /// /// @see BBMOD_StaticBatch function BBMOD_DynamicBatch(_model=undefined, _size=32, _slotsPerInstance=12) : BBMOD_Class() constructor { BBMOD_CLASS_GENERATED_BODY; static Class_destroy = destroy; /// @var {Struct.BBMOD_Model} A model that is being batched. /// @readonly Model = _model; /// @var {Struct.BBMOD_Model} The batched model. /// @readonly Batch = undefined; /// @var {Real} Number of model instances in the batch. /// @readonly Size = _size; /// @var {Real} Number of instances currently added to the dynamic batch. /// @readonly /// @see BBMOD_DynamicBatch.add_instance InstanceCount = 0; /// @var {Real} Number of slots that each instance takes in the data array. /// @readonly SlotsPerInstance = _slotsPerInstance; /// @var {Real} Total length of batch data array for a single draw call. /// @readonly BatchLength = Size * SlotsPerInstance; /// @var {Function} A function that writes instance data into the batch data /// array. It must take the instance, array and starting index as arguments! /// Defaults to {@link BBMOD_DynamicBatch.default_fn}. DataWriter = default_fn; /// @var {Array>} /// @private __data = []; /// @var {Array}} /// @private __ids = []; /// @var {Id.DsMap} Mapping from instances to indices at which they are /// stored in the data array. /// @private __instanceToIndex = ds_map_create(); /// @var {Id.DsMap} Mapping from data array indices to instances that they /// hold. /// @private __indexToInstance = ds_map_create(); // @func from_model(_model) /// /// @desc /// /// @param {Struct.BBMOD_Model} _model /// /// @return {Struct.BBMOD_DynamicBatch} Returns `self`. static from_model = function (_model) { Model = _model; build_batch(); return self; }; /// @func __resize_data() /// /// @desc Resizes `__data` and `__ids` arrays to required size. /// /// @private static __resize_data = function () { var _requiredArrayCount = ceil(InstanceCount / Size); var _currentArrayCount = array_length(__data); if (_currentArrayCount > _requiredArrayCount) { array_resize(__data, _requiredArrayCount); array_resize(__ids, _requiredArrayCount); } else if (_currentArrayCount < _requiredArrayCount) { repeat (_requiredArrayCount - _currentArrayCount) { array_push(__data, array_create(BatchLength, 0.0)); array_push(__ids, array_create(Size, 0.0)); } } }; /// @func add_instance(_instance) /// /// @desc Adds an instance to the dynamic batch. /// /// @param {Id.Instance, Struct} _instance The instance to be added. /// /// @return {Struct.BBMOD_DynamicBatch} Returns `self`. static add_instance = function (_instance) { var _indexIds = InstanceCount; var _indexData = _indexIds * SlotsPerInstance; __instanceToIndex[? _instance] = _indexData; __indexToInstance[? _indexData] = _instance; ++InstanceCount; __resize_data(); method(_instance, DataWriter)(__data[_indexData div BatchLength], _indexData mod BatchLength); __ids[_indexIds div Size][@ _indexIds mod Size] = real(_instance[$ "id"] ?? 0.0); return self; }; /// @func update_instance(_instance) /// /// @desc Updates batch data for given instance. /// /// @param {Id.Instance, Struct} _instance The instance to update. /// /// @return {Struct.BBMOD_DynamicBatch} Returns `self`. /// /// @see BBMOD_DynamicBatch.DataWriter static update_instance = function (_instance) { gml_pragma("forceinline"); var _index = __instanceToIndex[? _instance]; method(_instance, DataWriter)(__data[_index div BatchLength], _index mod BatchLength); return self; }; /// @func remove_instance(_instance) /// /// @desc Removes an instance from the dynamic batch. /// /// @param {Id.Instance, Struct} _instance The instance to remove. /// /// @return {Struct.BBMOD_DynamicBatch} Returns `self`. static remove_instance = function (_instance) { var _indexDataDeleted = __instanceToIndex[? _instance]; if (_indexDataDeleted != undefined) { var _indexIdDeleted = _indexDataDeleted / SlotsPerInstance; --InstanceCount; if (InstanceCount > 0) { //////////////////////////////////////////////////////////////// // Data // Get last used index var _indexLast = InstanceCount * SlotsPerInstance; // Get instance that is stored on that index var _instanceLast = __indexToInstance[? _indexLast]; // Find the exact array that stores the data var _dataLast = __data[_indexLast div BatchLength]; // Get starting index within that array var i = _indexLast mod BatchLength; // Copy data of the last instance over the data of the removed instance array_copy( __data[_indexDataDeleted div BatchLength], _indexDataDeleted mod BatchLength, _dataLast, i, SlotsPerInstance); // Clear slots repeat (SlotsPerInstance) { _dataLast[i++] = 0.0; } //////////////////////////////////////////////////////////////// // Ids // Get last used index var _indexLast = InstanceCount; // Find the exact array that stores the id var _idsLast = __ids[_indexLast div Size]; // Get starting index within that array var i = _indexLast mod Size; // Copy id of the last instance over the id of the removed instance __ids[_indexIdDeleted div Size][@ _indexIdDeleted mod Size] = _idsLast[i]; // Clear slots _idsLast[i] = 0.0; //////////////////////////////////////////////////////////////// // Last instance is now stored instead of the deleted one __instanceToIndex[? _instanceLast] = _indexDataDeleted; __indexToInstance[? _indexDataDeleted] = _instanceLast; ds_map_delete(__indexToInstance, _indexLast); } __resize_data(); } return self; }; /// @func submit([_materials[, _batchData]]) /// /// @desc Immediately submits the dynamic batch for rendering. /// /// @param {Array} [_materials] An array of materials. /// @param {Array, Array>} [_batchData] Data for dynamic /// batching. /// /// @return {Struct.BBMOD_DynamicBatch} Returns `self`. /// /// @see BBMOD_DynamicBatch.submit_object /// @see BBMOD_DynamicBatch.render /// @see BBMOD_DynamicBatch.render_object /// @see BBMOD_Material /// @see BBMOD_ERenderPass static submit = function (_materials=undefined, _batchData=undefined) { gml_pragma("forceinline"); _batchData ??= __data; if (array_length(_batchData) > 0) { if (_materials != undefined && !is_array(_materials)) { _materials = [_materials]; } matrix_set(matrix_world, matrix_build_identity()); Batch.submit(_materials, undefined, _batchData); } return self; }; /// @func render([_materials[, _batchData[, _ids]]]) /// /// @desc Enqueues the dynamic batch for rendering. /// /// @param {Array} [_materials] An array of materials. /// @param {Array, Array>} [_batchData] Data for dynamic /// batching. Defaults to data of instances added with /// {@link BBMOD_DynamicBatch.add_instance}. /// @param {Array, Array>} [_ids] IDs of /// instances in the `_batchData` array(s). Defaults to IDs of instances /// added with {@link BBMOD_DynamicBatch.add_instance}. Applicable only when /// `_batchData` is `undefined`! /// /// @return {Struct.BBMOD_DynamicBatch} Returns `self`. /// /// @see BBMOD_DynamicBatch.submit /// @see BBMOD_DynamicBatch.submit_object /// @see BBMOD_DynamicBatch.render_object /// @see BBMOD_Material static render = function (_materials=undefined, _batchData=undefined, _ids=undefined) { gml_pragma("forceinline"); if (_batchData == undefined) { _batchData = __data; global.__bbmodInstanceIDBatch = __ids; } else { global.__bbmodInstanceIDBatch = _ids; } if (array_length(_batchData) > 0) { if (_materials != undefined && !is_array(_materials)) { _materials = [_materials]; } matrix_set(matrix_world, matrix_build_identity()); Batch.render(_materials, undefined, _batchData); } return self; }; /// @func default_fn(_data, _index) /// /// @desc The default data writer function. Uses instance's variables /// `x`, `y`, `z` for position, `image_xscale` for uniform scale and /// `image_angle` for rotation around the `z` axis. /// /// @param {Array} _data An array to which the function will write /// instance data. The data layout is compatible with shader `BBMOD_ShDefaultBatched` /// and hence with material {@link BBMOD_MATERIAL_DEFAULT_BATCHED}. /// @param {Real} _index An index at which the first variable will be written. /// /// @see BBMOD_DynamicBatch.submit_object /// @see BBMOD_DynamicBatch.render_object static default_fn = function (_data, _index) { // Position _data[@ _index] = x; _data[@ _index + 1] = y; _data[@ _index + 2] = z; // Uniform scale _data[@ _index + 3] = image_xscale; // Rotation new BBMOD_Quaternion() .FromAxisAngle(BBMOD_VEC3_UP, image_angle) .ToArray(_data, _index + 4); // ID _data[@ _index + 8] = ((id & $000000FF) >> 0) / 255; _data[@ _index + 9] = ((id & $0000FF00) >> 8) / 255; _data[@ _index + 10] = ((id & $00FF0000) >> 16) / 255; _data[@ _index + 11] = ((id & $FF000000) >> 24) / 255; }; static __draw_object = function (_method, _object, _materials, _fn=undefined) { if (!instance_exists(_object)) { return; } _fn ??= DataWriter; var _slotsPerInstance = SlotsPerInstance; var _size = Size; var _dataSize = _size * _slotsPerInstance; var _data = array_create(_dataSize, 0.0); var _ids = array_create(_size, 0.0); var _indexData = 0; var _indexId = 0; var _batchData = [_data]; var _batchIds = [_ids]; with (_object) { method(self, _fn)(_data, _indexData); _indexData += _slotsPerInstance; _ids[@ _indexId++] = real(self[$ "id"] ?? 0.0); if (_indexData >= _dataSize) { _data = array_create(_dataSize, 0.0); _indexData = 0; array_push(_batchData, _data); _ids = array_create(_size, 0.0); _indexId = 0; array_push(_batchIds, _ids); } } _method(_material, _batchData, _batchIds); }; /// @func submit_object(_object[, _materials[, _fn]]) /// /// @desc Immediately submits all instances of an object for rendering in /// batches of {@link BBMOD_DynamicBatch.size}. /// /// @param {Real} _object An object to submit. /// @param {Array} [_material] An array of materials /// to use. /// @param {Function} [_fn] A function that writes instance data to an array /// which is then passed to the material's shader. Defaults to /// {@link BBMOD_DynamicBatch.default_fn} if `undefined`. /// /// @return {Struct.BBMOD_DynamicBatch} Returns `self`. /// /// @example /// ```gml /// carBatch.submit_object(OCar, [matCar], function (_data, _index) { /// // Position /// _data[@ _index] = x; /// _data[@ _index + 1] = y; /// _data[@ _index + 2] = z; /// // Uniform scale /// _data[@ _index + 3] = image_xscale; /// // Rotation /// new BBMOD_Quaternion() /// .FromAxisAngle(BBMOD_VEC3_UP, image_angle) /// .ToArray(_data, _index + 4); /// // ID /// _data[@ _index + 8] = ((id & $000000FF) >> 0) / 255; /// _data[@ _index + 9] = ((id & $0000FF00) >> 8) / 255; /// _data[@ _index + 10] = ((id & $00FF0000) >> 16) / 255; /// _data[@ _index + 11] = ((id & $FF000000) >> 24) / 255; /// }); /// ``` /// The function defined in this example is actually the implementation of /// {@link BBMOD_DynamicBatch.DataWriter}. You can use this to create you own /// variation of it. /// /// @see BBMOD_DynamicBatch.submit /// @see BBMOD_DynamicBatch.render /// @see BBMOD_DynamicBatch.render_object /// @see BBMOD_DynamicBatch.DataWriter static submit_object = function (_object, _materials=undefined, _fn=undefined) { gml_pragma("forceinline"); __draw_object(method(self, submit), _object, _materials, _fn); return self; }; /// @func render_object(_object[, _materials[, _fn]]) /// /// @desc Enqueues all instances of an object for rendering in batches of /// {@link BBMOD_DynamicBatch.size}. /// /// @param {Asset.GMObject} _object An object to render. /// @param {Array} [_materials] An array of materials /// to use. /// @param {Function} [_fn] A function that writes instance data to an /// array which is then passed to the material's shader. Defaults to /// {@link BBMOD_DynamicBatch.DataWriter} if `undefined`. /// /// @return {Struct.BBMOD_DynamicBatch} Returns `self`. /// /// @example /// ```gml /// carBatch.render_object(OCar, [matCar], function (_data, _index) { /// // Position /// _data[@ _index] = x; /// _data[@ _index + 1] = y; /// _data[@ _index + 2] = z; /// // Uniform scale /// _data[@ _index + 3] = image_xscale; /// // Rotation /// new BBMOD_Quaternion() /// .FromAxisAngle(BBMOD_VEC3_UP, image_angle) /// .ToArray(_data, _index + 4); /// // ID /// _data[@ _index + 8] = ((id & $000000FF) >> 0) / 255; /// _data[@ _index + 9] = ((id & $0000FF00) >> 8) / 255; /// _data[@ _index + 10] = ((id & $00FF0000) >> 16) / 255; /// _data[@ _index + 11] = ((id & $FF000000) >> 24) / 255; /// }); /// ``` /// The function defined in this example is actually the implementation of /// {@link BBMOD_DynamicBatch.default_fn}. You can use this to create your /// own variation of it. /// /// @see BBMOD_DynamicBatch.submit /// @see BBMOD_DynamicBatch.submit_object /// @see BBMOD_DynamicBatch.render /// @see BBMOD_DynamicBatch.DataWriter static render_object = function (_object, _materials=undefined, _fn=undefined) { gml_pragma("forceinline"); __draw_object(method(self, render), _object, _materials, _fn); return self; }; /// @func freeze() /// /// @desc Freezes the dynamic batch. This makes it render faster. /// /// @return {Struct.BBMOD_DynamicBatch} Returns `self`. static freeze = function () { gml_pragma("forceinline"); Batch.freeze(); return self; }; static build_batch = function () { if (Batch != undefined) { return; } Batch = Model.clone(); var _vertexFormatOld = Batch.VertexFormat; var _vertexFormatNew; if (_vertexFormatOld != undefined) { _vertexFormatNew = new BBMOD_VertexFormat({ Vertices: _vertexFormatOld.Vertices, Normals: _vertexFormatOld.Normals, TextureCoords: _vertexFormatOld.TextureCoords, TextureCoords2: _vertexFormatOld.TextureCoords2, Colors: _vertexFormatOld.Colors, TangentW: _vertexFormatOld.TangentW, Bones: _vertexFormatOld.Bones, Ids: true, }); Batch.VertexFormat = _vertexFormatNew; } for (var i = array_length(Batch.Meshes) - 1; i >= 0; --i) { var _mesh = Batch.Meshes[i]; var _meshVertexFormatOld = _mesh.VertexFormat ?? _vertexFormatOld; var _byteSizeOld = _meshVertexFormatOld.get_byte_size(); var _meshVertexFormatNew; if (_mesh.VertexFormat) { _meshVertexFormatNew = new BBMOD_VertexFormat({ Vertices: _meshVertexFormatOld.Vertices, Normals: _meshVertexFormatOld.Normals, TextureCoords: _meshVertexFormatOld.TextureCoords, TextureCoords2: _meshVertexFormatOld.TextureCoords2, Colors: _meshVertexFormatOld.Colors, TangentW: _meshVertexFormatOld.TangentW, Bones: _meshVertexFormatOld.Bones, Ids: true, }); } else { _meshVertexFormatNew = _vertexFormatNew; } var _byteSizeNew = _meshVertexFormatNew.get_byte_size(); var _vertexBufferOld = _mesh.VertexBuffer; var _bufferOld = buffer_create_from_vertex_buffer(_vertexBufferOld, buffer_fixed, 1); var _vertexCount = buffer_get_size(_bufferOld) / _byteSizeOld; var _bufferNew = buffer_create(Size * _vertexCount * _byteSizeNew, buffer_fixed, 1); var _offsetNew = 0; var _sizeOfF32 = buffer_sizeof(buffer_f32); var _id = 0; repeat (Size) { var _offsetOld = 0; repeat (_vertexCount) { buffer_copy(_bufferOld, _offsetOld, _byteSizeOld, _bufferNew, _offsetNew); _offsetOld += _byteSizeOld; _offsetNew += _byteSizeOld; buffer_poke(_bufferNew, _offsetNew, buffer_f32, _id); _offsetNew += _sizeOfF32; } ++_id; } _mesh.VertexBuffer = vertex_create_buffer_from_buffer(_bufferNew, _meshVertexFormatNew.Raw); _mesh.VertexFormat = _meshVertexFormatNew; buffer_delete(_bufferNew); vertex_delete_buffer(_vertexBufferOld); buffer_delete(_bufferOld); } }; static destroy = function () { Class_destroy(); if (Batch != undefined) { Batch = Batch.destroy(); } __data = undefined; ds_map_destroy(__instanceToIndex); ds_map_destroy(__indexToInstance); return undefined; }; if (Model != undefined) { build_batch(); } }