import "luxe: io" for IO import "luxe: world" for World, Entity, Modifiers, ModifierSystem, Transform import "luxe: draw" for Draw, PathStyle, LineCap import "luxe: color" for Color import "luxe: render" for Geometry, Primitive, IndexType, Render, Material import "luxe: assets" for Assets, Strings import "luxe: bytes" for Uint16, Floats import "luxe: math" for Math import "trail: _queue" for Queue import "trail: modifier/trail.modifier" for ModifierData #doc=""" Modifier to add simple trails to entities. Needs a Transform on the same entity to work. ```js var entity = Entity.create(world) Transform.create(entity) Trail.create(entity) //now just move and rotate the entity around ``` """ class Trail { static time_based{true} static distance_based{false} #doc="Add a trail modifier to an entity. Entity should also have a Transform." #args( entity = "The entity to add the trail modifier to." ) static create(entity: Entity): None { Modifiers.create(This, entity) } #doc="Remove the trail modifier from an entity." #args( entity = "The entity to remove the trail modifier from." ) static destroy(entity: Entity): None { Modifiers.destroy(This, entity) } #doc="Check whether an entity has a trail modifier or not." #args( entity = "The entity to check whether it has a trail modifier." ) static has(entity: Entity): Bool { return Modifiers.has(This, entity) } #doc="Reset trail to no length." #args( entity = "The entity with the trail modifier." ) static reset(entity: Entity): None { Modifiers.get_system(This, entity).reset(entity ) } #doc="Set the width of a trail." #args( entity = "The entity with the trail modifier.", width = "The new width of the trail." ) static set_width(entity: Entity, width: Num): None { Modifiers.get(This, entity).edge_length = width } #doc="Get the width of a trail." #args( entity = "The entity with the trail modifier." ) static get_width(entity: Entity): Num { return Modifiers.get(This, entity).edge_length } #doc="Set the length of a trail." #args( entity = "The entity with the trail modifier.", length = "The new length of the trail." ) static set_length(entity: Entity, length: Num): None { Modifiers.get(This, entity).length = length //todo: recalculate transient data? } #doc="Get the length of a trail." #args( entity = "The entity with the trail modifier." ) static get_length(entity: Entity): Num { return Modifiers.get(This, entity).length } #doc=""" Set whether the trail is time based or distance based. ```js Trail.set_time_based(entity, Trail.distance_based) ``` """ #args( entity = "The entity with the trail modifier.", time_based = "`true` if its time based, `false` if its distance based." ) static set_time_based(entity: Entity, time_based: Bool): None { Modifiers.get(This, entity).time_based = time_based } #doc=""" Get whether the trail is time based or distance based. (true if time based, false if distance based) """ #args( entity = "The entity with the trail modifier." ) static get_time_based(entity: Entity): Bool { return Modifiers.get(This, entity).time_based } #doc=""" Set whether the trail should have normalized UVs of a trail. If the UVs are normalized, they should be linear over distance, especially with time based trails you otherwise can get very wobbly behaviour as speed changes. """ #args( entity = "The entity with the trail modifier.", length = "Whether the trail should have normalized uvs." ) static set_normalize_uvs(entity: Entity, normalize_uvs: Bool): None { Modifiers.get(This, entity).normalize_uvs = normalize_uvs } #doc="Get whether a trail has normalized uvs." #args( entity = "The entity with the trail modifier." ) static get_normalize_uvs(entity: Entity): Bool { return Modifiers.get(This, entity).normalize_uvs } #doc="Set the material the trail is rendered with via its id." #args( entity = "The entity with the trail modifier." ) static set_material_id(entity: Entity, material_id: String): None { Strings.add(material_id) Modifiers.get(This, entity).material = material_id Modifiers.get_system(This, entity).set_material_id(entity, material_id) } #doc="Set the material the trail is rendered with." #args( entity = "The entity with the trail modifier." ) static set_material(entity: Entity, material: Material): None { var material_id = Material.get_source_id(material) Strings.add(material_id) Modifiers.get(This, entity).material = material_id Modifiers.get_system(This, entity).set_material(entity, material) } #doc="Get the id of the currently used material." #args( entity = "The entity with the trail modifier." ) static get_material_id(entity: Entity): String { return Strings.get(Modifiers.get(This, entity).material) } #doc=""" Set mesh subdivisions around length of the trail. This will rebuild the mesh entirely and potentially throw away custom materials you set via `set_material`. """ #args( entity = "The entity with the trail modifier.", subdivisions = "Subdivisions along length." ) static set_subdivisions_length(entity: Entity, subdivisions: Num){ Modifiers.get(This, entity).subdivisions_length = subdivisions Modifiers.get_system(This, entity).recreate_buffers(entity) } #doc="Get subdivisions in length of trail." #args( entity = "The entity with the trail modifier." ) static get_subdivisions_length(entity: Entity): Num{ return Modifiers.get(This, entity).subdivisions_length } #doc=""" Set mesh subdivisions around length of the trail. This will rebuild the mesh entirely and potentially throw away custom materials you set via `set_material`. """ #args( entity = "The entity with the trail modifier.", subdivisions = "Subdivisions along width." ) static set_subdivisions_width(entity: Entity, subdivisions: Num){ Modifiers.get(This, entity).subdivisions_width = subdivisions Modifiers.get_system(This, entity).recreate_buffers(entity) } #doc="Get subdivisions in width of trail." #args( entity = "The entity with the trail modifier." ) static get_subdivisions_width(entity: Entity): Num{ return Modifiers.get(This, entity).subdivisions_width } } //Trail #hidden class Point { construct new(pos: Vec, up: Vec){ _pos = pos _up = up } pos{_pos} up{_up} } #hidden class TrailData { construct new(geometry, indices, positions, colors, uvs){ _geometry = geometry _index_buf = indices _pos_buf = positions _col_buf = colors _uv_buf = uvs _progress = 0 _progress_since_last_point = 0 _points = Queue.new() } geometry{_geometry} index_buffer{_index_buf} position_buffer{_pos_buf} color_buffer{_col_buf} uv_buffer{_uv_buf} points: Queue {_points} progress: Num {_progress} progress=(v){_progress=v} progress_since_last_point: Num {_progress_since_last_point} progress_since_last_point=(v){_progress_since_last_point = v} previous_pos: Vec {_previous_pos} previous_pos=(v){_previous_pos=v} previous_up: Vec {_previous_up} previous_up=(v){_previous_up} } //Your modifier system implementation. //This speaks to the engine and your user facing API //to do the actual work. You'll get notified when things change //in the world and respond to them here. #hidden class TrailSystem is ModifierSystem { construct new() { //called when your system is first created. _instance_data = {} } init(world) { _world = world _draw = Draw.create(World.render_set(world)) var edge_debug = PathStyle.new() edge_debug.color = [1,1,1,1] edge_debug.thickness = 2 edge_debug.cap = LineCap.round _debug_edge_style = edge_debug var path_debug = PathStyle.new() path_debug.color = [1, 0, 0, 1] path_debug.thickness = 1 _debug_path_style = path_debug var mesh_debug = PathStyle.new() mesh_debug.color = [0, 0, 1, 1] mesh_debug.thickness = 0.5 _debug_mesh_style = mesh_debug } reset(entity: Entity){ _instance_data[entity].points.clear() } set_material_id(entity: Entity, material_id: String){ var material = Assets.material(material_id) set_material(entity, material) } set_material(entity: Entity, material: Material){ var data = _instance_data[entity] if(!data || !material) return Geometry.set_material(entity, material) } recreate_buffers(entity: Entity){ //better solution for after fixes /*var data = get(entity) var inst_data: TrailData = _instance_data[entity] if(!data || !inst_data) return inst_data.points.clear() var index_data = create_index_buffer(data.subdivisions_length, data.subdivisions_width) Render.index_buffer_replace(inst_data.index_buffer, index_data, index_data.length) var pos_data = create_position_buffer(data.subdivisions_length, data.subdivisions_width) Render.vertex_buffer_replace(inst_data.position_buffer, pos_data, pos_data.length) var uv_data = create_uv_buffer(data.subdivisions_length, data.subdivisions_width) Render.vertex_buffer_replace(inst_data.uv_buffer, uv_data, uv_data.length) var color_data = create_color_buffer(data.subdivisions_length, data.subdivisions_width) Render.vertex_buffer_replace(inst_data.color_buffer, color_data, color_data.length)*/ //...but for now... var data = get(entity) detach(entity, data) attach(entity, data) } destroy() { Draw.destroy(_draw) } attach(entity, data: ModifierData) { var material_id = Strings.get(data.material) var material = Assets.material(material_id) var index_buffer = create_index_buffer(data.subdivisions_length, data.subdivisions_width) index_buffer = Render.create_index_buffer(index_buffer, index_buffer.length) var geo = Geometry.create(Primitive.triangle, material, (data.subdivisions_length - 1) * 6, IndexType.u16, index_buffer) var pos_buffer = create_position_buffer(data.subdivisions_length, data.subdivisions_width) pos_buffer = Render.create_vertex_buffer(pos_buffer, pos_buffer.length) Geometry.set_vertex_buffer(geo, 0, pos_buffer) var color_buffer = create_color_buffer(data.subdivisions_length, data.subdivisions_width) color_buffer = Render.create_vertex_buffer(color_buffer, color_buffer.length) Geometry.set_vertex_buffer(geo, 1, color_buffer) var uv_buffer = create_uv_buffer(data.subdivisions_length, data.subdivisions_width) uv_buffer = Render.create_vertex_buffer(uv_buffer, uv_buffer.length) Geometry.set_vertex_buffer(geo, 2, uv_buffer) World.render_set_add(_world, geo, entity) _instance_data[entity] = TrailData.new(geo, index_buffer, pos_buffer, color_buffer, uv_buffer) } create_position_buffer(subdivs_len: Num, subdivs_width: Num): Floats{ var raw = Floats.new(4 * subdivs_width * subdivs_len) return raw } create_color_buffer(subdivs_len: Num, subdivs_width: Num): Floats{ var raw = Floats.new(4 * subdivs_width * subdivs_len) for(i in 0...subdivs_len){ for(ii in 0...subdivs_width){ raw[i*subdivs_width*4 + ii*4 + 0] = 1 raw[i*subdivs_width*4 + ii*4 + 1] = 1 raw[i*subdivs_width*4 + ii*4 + 2] = 1 raw[i*subdivs_width*4 + ii*4 + 3] = 1 } } //var arr = (0...(raw.size/4)).map{|i|"[%(raw[i*4]), %(raw[i*4+1]), %(raw[i*4+2])]"}.join("\n") //System.print(arr) return raw } create_uv_buffer(subdivs_len: Num, subdivs_width: Num): Floats{ var raw = Floats.new(2 * subdivs_width * subdivs_len) for(i in 0...subdivs_len){ var x = i / (subdivs_len - 1) for(ii in 0...subdivs_width){ var y = ii / (subdivs_width - 1) raw[i*2*subdivs_width + ii*2 + 0] = x raw[i*2*subdivs_width + ii*2 + 1] = y } } //var arr = (0...(raw.size/2)).map{|i|"[%(raw[i*2]), %(raw[i*2+1])]"}.join("\n") //System.print(arr) return raw } create_index_buffer(subdivs_len: Num, subdivs_width: Num): Uint16{ var rows = subdivs_len - 1 var cols = subdivs_width-1 var index_count = rows * cols * 6 var raw = Uint16.new(index_count) for(i in 0...rows){ for(ii in 0...cols){ //System.print("x: %(i) | y:%(ii)") //first triangle raw[i*6*cols + ii*6 + 0] = i * (cols+1) + ii + 0 raw[i*6*cols + ii*6 + 1] = i * (cols+1) + ii + cols+1 raw[i*6*cols + ii*6 + 2] = i * (cols+1) + ii + 1 //System.print("(%(i*6*cols + ii*6 + 0), %(i*6*cols + ii*6 + 1), %(i*6*cols + ii*6 + 2))") //System.print("[%(i * (cols+1) + ii + 0), %(i * (cols+1) + ii + cols+1), %(i * (cols+1) + ii + 1)]") //second triangle raw[i*6*cols + ii*6 + 3] = i * (cols+1) + ii + 1 raw[i*6*cols + ii*6 + 4] = i * (cols+1) + ii + cols+1 raw[i*6*cols + ii*6 + 5] = i * (cols+1) + ii + cols+2 //System.print("(%(i*6*cols + ii*6 + 3), %(i*6*cols + ii*6 + 4), %(i*6*cols + ii*6 + 5))") //System.print("[%(i * (cols+1) + ii + 1), %(i * (cols+1) + ii + cols+1), %(i * (cols+1) + ii + cols+2)]") } } //var arr = (0...(raw.length/(3*4))).map{|i|"[%(raw[i*3]), %(raw[i*3+1]), %(raw[i*3+2])]"}.join("\n") //System.print(arr) return raw } detach(entity, data: ModifierData) { var inst_data: TrailData = _instance_data[entity] World.render_set_remove(_world, inst_data.geometry, entity) Geometry.destroy(inst_data.geometry) Render.destroy_index_buffer(inst_data.index_buffer) Render.destroy_vertex_buffer(inst_data.position_buffer) Render.destroy_vertex_buffer(inst_data.color_buffer) Render.destroy_vertex_buffer(inst_data.uv_buffer) _instance_data.remove(entity) } tick(delta) { if(World.tag_has(_world, "edit")){ each{|entity, data: ModifierData| if(!Transform.has(entity)) return var start = Transform.local_point_to_world(entity, 0, data.edge_length * -0.5, 0) var end = Transform.local_point_to_world(entity, 0, data.edge_length * 0.5, 0) Draw.path3D(_draw, [start, end], _debug_edge_style, false) } Draw.commit(_draw) return } //System.print("positions") record_positions(delta) //System.print("buffers") update_buffers() //System.print("debug") //debug_draw() //System.print("end") } //tick record_positions(delta: Num){ each{|entity, data: ModifierData| if(!Transform.has(entity)) return var instance_data: TrailData = _instance_data[entity] var pos = Transform.get_pos_world(entity) var up = Transform.local_dir_to_world(entity, 0, data.edge_length * 0.5, 0) var prev_pos = instance_data.previous_pos || pos var prev_up = instance_data.previous_up || up var dist = instance_data.progress_since_last_point var diff:Num if(data.time_based){ diff = delta } else { diff = Math.dist(pos, prev_pos) } if(diff > data.length){ if(data.time_based){ var t = data.length / diff prev_pos.x = Math.lerp(pos.x, prev_pos.x, t) prev_pos.y = Math.lerp(pos.y, prev_pos.y, t) prev_pos.z = Math.lerp(pos.z, prev_pos.z, t) } else { var dir = [prev_pos.x - pos.x, prev_pos.y - pos.y, prev_pos.z - pos.z] Math.normalize(dir) dir.x = dir.x * data.length dir.y = dir.y * data.length dir.z = dir.z * data.length prev_pos.x = pos.x + dir.x prev_pos.y = pos.y + dir.y prev_pos.z = pos.z + dir.z } diff = data.length } dist = dist + diff var step = data.length / data.subdivisions_length while(dist > step){ dist = dist - step var t = 1 - dist / diff var point = Point.new([ Math.lerp(prev_pos.x, pos.x, t), Math.lerp(prev_pos.y, pos.y, t), Math.lerp(prev_pos.z, pos.z, t) ], [ Math.lerp(prev_up.x, up.x, t), Math.lerp(prev_up.y, up.y, t), Math.lerp(prev_up.z, up.z, t) ]) Math.normalize(point.up) if(instance_data.points.count >= data.subdivisions_length) instance_data.points.dequeue() instance_data.points.enqueue(point) } instance_data.progress_since_last_point = dist instance_data.previous_pos = pos instance_data.previous_up = up } } update_buffers(){ each{|entity, data: ModifierData| if(!Transform.has(entity)) return var instance_data: TrailData = _instance_data[entity] var points = instance_data.points var pos = Transform.get_pos_world(entity) var positions = Floats.new(Render.vertex_buffer_get_size(instance_data.position_buffer) / 4) Render.vertex_buffer_get_data(instance_data.position_buffer, positions, positions.size * 4, 0) var uvs: Floats if(data.normalize_uvs){ uvs = Floats.new(Render.vertex_buffer_get_size(instance_data.uv_buffer) / 4) Render.vertex_buffer_get_data(instance_data.uv_buffer, uvs, uvs.size * 4, 0) } //System.print("positions(%(Render.vertex_buffer_get_size(instance_data.position_buffer))):") //System.print((0...(positions.size/4)).map{|i|"[%(positions[i*4]), %(positions[i*4+1]), %(positions[i*4+2])]"}.join("\n")) var i = -1 { i = i + 1 var start = Transform.local_point_to_world(entity, 0, data.edge_length * -0.5, 0) var end = Transform.local_point_to_world(entity, 0, data.edge_length * 0.5, 0) for(ii in 0...data.subdivisions_width){ var t = ii / (data.subdivisions_width-1) var pos_x = Math.lerp(start.x, end.x, t) var pos_y = Math.lerp(start.y, end.y, t) var pos_z = Math.lerp(start.z, end.z, t) positions[i*4*data.subdivisions_width + ii*4 + 0] = pos_x positions[i*4*data.subdivisions_width + ii*4 + 1] = pos_y positions[i*4*data.subdivisions_width + ii*4 + 2] = pos_z } } var length = 0 var current_len = 0 if(data.normalize_uvs){ for(i in 0...points.count){ var from = i<(points.count-1)? points[i+1].pos : pos var to = points[i].pos var dist = Math.dist(from, to) length = length + (i != (points.count-1) ? dist : dist * (1 - instance_data.progress_since_last_point / (data.length / data.subdivisions_length))) } current_len = points.count > 1 ? Math.dist(points[points.count-1].pos, pos) : 0 } var step = data.length / data.subdivisions_length var t = 1 - instance_data.progress_since_last_point / step var point_index = points.count - 1 while(true){ if(point_index < 0) break var p:Point var next:Point point_index = point_index - 1 if(point_index < 0) break p = points[point_index] next = points[point_index + 1] var to = p.pos var to_up = p.up var from = next.pos var from_up = next.up var pos = [ Math.lerp(from.x, to.x, t), Math.lerp(from.y, to.y, t), Math.lerp(from.z, to.z, t) ] var up = [ Math.lerp(from_up.x, to_up.x, t), Math.lerp(from_up.y, to_up.y, t), Math.lerp(from_up.z, to_up.z, t) ] Math.normalize(up) i = i + 1 for(y in 0...data.subdivisions_width){ var t = y / (data.subdivisions_width-1) var offset_x = Math.lerp(-up.x, up.x, t) var offset_y = Math.lerp(-up.y, up.y, t) var offset_z = Math.lerp(-up.z, up.z, t) positions[i*4*data.subdivisions_width + y*4 + 0] = pos.x + offset_x * data.edge_length * 0.5 positions[i*4*data.subdivisions_width + y*4 + 1] = pos.y + offset_y * data.edge_length * 0.5 positions[i*4*data.subdivisions_width + y*4 + 2] = pos.z + offset_z * data.edge_length * 0.5 } if(data.normalize_uvs){ var segment_len = Math.dist(from, to) current_len = current_len + segment_len * t var x = current_len / length for(ii in 0...data.subdivisions_width){ var y = ii / (data.subdivisions_width - 1) uvs[i*2*data.subdivisions_width + ii*2 + 0] = x uvs[i*2*data.subdivisions_width + ii*2 + 1] = y } current_len = current_len + segment_len * (1-t) } } Render.vertex_buffer_replace(instance_data.position_buffer, positions, positions.length) if(data.normalize_uvs){ Render.vertex_buffer_replace(instance_data.uv_buffer, uvs, uvs.length) } Geometry.set_vert_count(instance_data.geometry, i.max(0) * 6 * (data.subdivisions_width-1)) } } debug_draw(){ each {|entity, data: ModifierData| var start = [0, 0] var end = [0, data.edge_length] var depth = 0 if(Transform.has(entity)){ start = Transform.local_point_to_world(entity, 0, data.edge_length * -0.5, 0) end = Transform.local_point_to_world(entity, 0, data.edge_length * 0.5, 0) depth = Transform.get_depth2D_world(entity) } Draw.line(_draw, start.x, start.y, end.x, end.y, depth, _debug_edge_style) var instance_data: TrailData = _instance_data[entity] var points = instance_data.points if(points.count > 1){ Draw.path3D(_draw, points.map{|p:Point|p.pos}.toList, _debug_path_style, false) } points.each{|point: Point| var from_x = point.pos.x + point.up.x * data.edge_length * 0.5 var from_y = point.pos.y + point.up.y * data.edge_length * 0.5 var from_z = point.pos.x + point.up.x * data.edge_length * 0.5 var to_x = point.pos.x - point.up.x * data.edge_length * 0.5 var to_y = point.pos.y - point.up.y * data.edge_length * 0.5 var to_z = point.pos.z - point.up.z * data.edge_length * 0.5 Draw.path3D(_draw, [[from_x, from_y, from_z], [to_x, to_y, to_z]], _debug_path_style, false) } var positions = Floats.new(Render.vertex_buffer_get_size(instance_data.position_buffer) / 4) Render.vertex_buffer_get_data(instance_data.position_buffer, positions, positions.size * 4, 0) var indices = Uint16.new(Render.index_buffer_get_size(instance_data.index_buffer) / 2) Render.index_buffer_get_data(instance_data.index_buffer, indices, indices.length, 0) for(i in 0...(Geometry.get_vert_count(instance_data.geometry) / 3)){ var i1 = indices[i * 3 + 0] var i2 = indices[i * 3 + 1] var i3 = indices[i * 3 + 2] var v1 = [positions[i1 * 4 + 0], positions[i1 * 4 + 1], positions[i1 * 4 + 2]] var v2 = [positions[i2 * 4 + 0], positions[i2 * 4 + 1], positions[i2 * 4 + 2]] var v3 = [positions[i3 * 4 + 0], positions[i3 * 4 + 1], positions[i3 * 4 + 2]] Draw.path3D(_draw, [v1, v2, v3], _debug_mesh_style, true) } } Draw.commit(_draw) } } //TrailSystem var Modifier = TrailSystem //required