// Copyright 2025 International Digital Economy Academy
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
///|
priv struct LightingShaderState {
shader : @raylib.Shader
ambient_light_loc : Int
view_position_loc : Int
view_forward_loc : Int
directional_count_loc : Int
directional_directions_loc : Int
directional_colors_loc : Int
point_count_loc : Int
point_positions_loc : Int
point_colors_loc : Int
point_ranges_loc : Int
spot_count_loc : Int
spot_positions_loc : Int
spot_directions_loc : Int
spot_colors_loc : Int
spot_params_loc : Int
material_base_color_loc : Int
material_emissive_color_loc : Int
material_normal_scale_loc : Int
material_occlusion_strength_loc : Int
material_metallic_loc : Int
material_roughness_loc : Int
material_alpha_mode_loc : Int
material_alpha_cutoff_loc : Int
material_has_base_texture_loc : Int
material_has_emissive_texture_loc : Int
material_has_metallic_roughness_texture_loc : Int
material_has_occlusion_texture_loc : Int
material_has_normal_texture_loc : Int
material_has_tangents_loc : Int
material_unlit_loc : Int
material_receive_shadows_loc : Int
shadow_enabled_locs : Array[Int]
shadow_cascade_count_locs : Array[Int]
shadow_matrix_locs : Array[Int]
shadow_rect_locs : Array[Int]
shadow_bound_locs : Array[Int]
shadow_depth_bias_locs : Array[Int]
shadow_normal_bias_locs : Array[Int]
shadow_sampler_locs : Array[Int]
directional_shadow_texel_size_loc : Int
spot_shadow_enabled_locs : Array[Int]
spot_shadow_matrix_locs : Array[Int]
spot_shadow_rect_locs : Array[Int]
spot_shadow_depth_bias_locs : Array[Int]
spot_shadow_normal_bias_locs : Array[Int]
spot_shadow_sampler_loc : Int
spot_shadow_texel_size_loc : Int
point_shadow_enabled_locs : Array[Int]
point_shadow_depth_bias_locs : Array[Int]
point_shadow_normal_bias_locs : Array[Int]
point_shadow_matrix_locs : Array[Int]
point_shadow_rect_locs : Array[Int]
point_shadow_sampler_loc : Int
point_shadow_texel_size_loc : Int
base_uv_offset_scale_loc : Int
base_uv_rotation_loc : Int
base_uv_set_loc : Int
metallic_roughness_uv_offset_scale_loc : Int
metallic_roughness_uv_rotation_loc : Int
metallic_roughness_uv_set_loc : Int
normal_uv_offset_scale_loc : Int
normal_uv_rotation_loc : Int
normal_uv_set_loc : Int
occlusion_uv_offset_scale_loc : Int
occlusion_uv_rotation_loc : Int
occlusion_uv_set_loc : Int
emissive_uv_offset_scale_loc : Int
emissive_uv_rotation_loc : Int
emissive_uv_set_loc : Int
}
///|
priv struct MaterialTextureBundle3D {
base_texture : @raylib.Texture
emissive_texture : @raylib.Texture
metallic_roughness_texture : @raylib.Texture
occlusion_texture : @raylib.Texture
normal_texture : @raylib.Texture
has_base_texture : Bool
has_emissive_texture : Bool
has_metallic_roughness_texture : Bool
has_occlusion_texture : Bool
has_normal_texture : Bool
base_transform : @render3d_types.TextureTransform3D
emissive_transform : @render3d_types.TextureTransform3D
metallic_roughness_transform : @render3d_types.TextureTransform3D
occlusion_transform : @render3d_types.TextureTransform3D
normal_transform : @render3d_types.TextureTransform3D
base_texcoord_set : Int
emissive_texcoord_set : Int
metallic_roughness_texcoord_set : Int
occlusion_texcoord_set : Int
normal_texcoord_set : Int
}
///|
priv struct MaterialTextureSource3D {
texture : @raylib.Texture
texcoord_set : Int
transform : @render3d_types.TextureTransform3D
}
///|
priv struct MaterialTextureSlotBinding {
shader_loc : Int
uniform_name : String
slot : Int
material_map : Int
}
///|
priv struct CosmicRasterImage {
width : Int
height : Int
pixels : Array[Int]
}
///|
priv struct CosmicTextTexture {
texture : @raylib.Texture
width : Int
height : Int
}
///|
priv struct RaylibBackend {
mut time_scale : Double
mut image_smooth : Bool
mut realtime_delta : Double
mut embedded_asset_lookup : (String) -> Bytes?
mut key_events : Set[@inputs.Code]?
mut mouse_state : @inputs.Mouse?
mut mouse_movement : @inputs.MouseMovement?
mut mouse_wheel : @inputs.MouseWheel?
mut gamepads_state : Set[@inputs.Gamepad]?
mut gamepad_buttons_state : Set[@inputs.GamepadButtonInput]?
mut gamepad_axes_state : Map[@inputs.GamepadAxisInput, Double]?
mut lock_state : Ref[Bool]?
mut lock_requested : Bool
mut cursor_locked : Bool
mut close_requested : Bool
mut frame_has_draw_commands : Bool
textures : Map[String, @raylib.Texture]
sounds : Map[String, @raylib.Sound]
audio_instances : Map[AudioInstance, AudioPlayback]
mut next_audio_instance : UInt
font_alias : Map[String, String]
fonts_by_path : Map[String, @raylib.Font]
cosmic_loaded_families : Set[String]
cosmic_textures : Map[String, CosmicTextTexture]
mut embedded_asset_cache : Map[String, Bytes]
meshes3d : Map[@render3d_types.MeshHandle, @render3d_types.MeshAsset]
materials3d : Map[
@render3d_types.MaterialHandle,
@render3d_types.StandardMaterial3D,
]
images3d : Map[@render3d_types.ImageHandle, @render3d_types.ImageAsset]
primitive_meshes : Map[String, @raylib.Mesh]
triangle_mesh_cache : Map[String, @raylib.Mesh]
mut lighting_shader : LightingShaderState?
mut lit_material : @raylib.Material?
mut lit_textured_material : @raylib.Material?
mut shadow_shader : ShadowShaderState?
mut shadow_material : @raylib.Material?
shadow_render_textures : Array[@raylib.RenderTexture]
directional_shadow_tile_sizes : Array[Int]
directional_shadow_atlas_columns : Array[Int]
directional_shadow_atlas_rows : Array[Int]
mut spot_shadow_render_texture : @raylib.RenderTexture?
mut spot_shadow_map_size : Int
mut point_shadow_render_texture : @raylib.RenderTexture?
mut point_shadow_map_size : Int
texture_warning_keys : Set[String]
}
///|
pub type AudioInstance = Int
///|
priv enum AudioPlaybackKind {
Sound(@raylib.Sound)
Music(@raylib.Music)
}
///|
priv struct AudioPlayback {
kind : AudioPlaybackKind
mut loop_ : Bool
mut paused : Bool
mut started : Bool
mut finished : Bool
}
///|
let backend : RaylibBackend = {
time_scale: 1.0,
image_smooth: true,
realtime_delta: 0.016,
embedded_asset_lookup: default_embedded_asset_lookup,
key_events: None,
mouse_state: None,
mouse_movement: None,
mouse_wheel: None,
gamepads_state: None,
gamepad_buttons_state: None,
gamepad_axes_state: None,
lock_state: None,
lock_requested: false,
cursor_locked: false,
close_requested: false,
frame_has_draw_commands: false,
textures: Map::new(),
sounds: Map::new(),
audio_instances: Map::new(),
next_audio_instance: 1U,
font_alias: Map::new(),
fonts_by_path: Map::new(),
cosmic_loaded_families: Set::new(),
cosmic_textures: Map::new(),
embedded_asset_cache: Map::new(),
meshes3d: Map::new(),
materials3d: Map::new(),
images3d: Map::new(),
primitive_meshes: Map::new(),
triangle_mesh_cache: Map::new(),
lighting_shader: None,
lit_material: None,
lit_textured_material: None,
shadow_shader: None,
shadow_material: None,
shadow_render_textures: [],
directional_shadow_tile_sizes: [],
directional_shadow_atlas_columns: [],
directional_shadow_atlas_rows: [],
spot_shadow_render_texture: None,
spot_shadow_map_size: 0,
point_shadow_render_texture: None,
point_shadow_map_size: 0,
texture_warning_keys: Set::new(),
}
///|
let clip_stack : Array[@smath.Rect] = []
///|
let cosmic_font_system : Ref[@cosmic.FontSystem] = Ref::new(
@cosmic.FontSystem::new(),
)
///|
let cosmic_swash_cache : Ref[@cosmic.SwashCache] = Ref::new(
@cosmic.SwashCache::new(),
)
///|
const MAX_DIRECTIONAL_LIGHTS : Int = 4
///|
const MAX_DIRECTIONAL_CASCADES : Int = 4
///|
const MAX_POINT_LIGHTS : Int = 8
///|
const MAX_SPOT_LIGHTS : Int = 4
///|
let raylib_lighting_vertex_shader : String = "#version 330\n" +
"in vec3 vertexPosition;\n" +
"in vec2 vertexTexCoord;\n" +
"in vec2 vertexTexCoord2;\n" +
"in vec3 vertexNormal;\n" +
"in vec4 vertexTangent;\n" +
"in vec4 vertexColor;\n" +
"uniform mat4 mvp;\n" +
"uniform mat4 matModel;\n" +
"uniform mat4 matNormal;\n" +
"out vec3 fragPosition;\n" +
"out vec2 fragTexCoord;\n" +
"out vec2 fragTexCoord2;\n" +
"out vec3 fragNormal;\n" +
"out vec4 fragTangent;\n" +
"out vec4 fragVertexColor;\n" +
"void main() {\n" +
" vec4 worldPosition = matModel * vec4(vertexPosition, 1.0);\n" +
" fragPosition = worldPosition.xyz;\n" +
" fragTexCoord = vertexTexCoord;\n" +
" fragTexCoord2 = vertexTexCoord2;\n" +
" fragNormal = normalize((matNormal * vec4(vertexNormal, 0.0)).xyz);\n" +
" vec3 tangent = normalize((matNormal * vec4(vertexTangent.xyz, 0.0)).xyz);\n" +
" fragTangent = vec4(tangent, vertexTangent.w);\n" +
" fragVertexColor = vertexColor;\n" +
" gl_Position = mvp * vec4(vertexPosition, 1.0);\n" +
"}\n"
///|
let raylib_lighting_fragment_shader : String = "#version 330\n" +
"in vec3 fragPosition;\n" +
"in vec2 fragTexCoord;\n" +
"in vec2 fragTexCoord2;\n" +
"in vec3 fragNormal;\n" +
"in vec4 fragTangent;\n" +
"in vec4 fragVertexColor;\n" +
"uniform sampler2D texture0;\n" +
"uniform sampler2D texture1;\n" +
"uniform sampler2D texture2;\n" +
"uniform sampler2D texture4;\n" +
"uniform sampler2D texture5;\n" +
"uniform vec3 ambientLight;\n" +
"uniform vec3 viewPosition;\n" +
"uniform vec3 viewForward;\n" +
"uniform int directionalCount;\n" +
"uniform vec3 directionalDirections[4];\n" +
"uniform vec3 directionalColors[4];\n" +
"uniform int pointCount;\n" +
"uniform vec3 pointPositions[8];\n" +
"uniform vec3 pointColors[8];\n" +
"uniform float pointRanges[8];\n" +
"uniform int spotCount;\n" +
"uniform vec3 spotPositions[4];\n" +
"uniform vec3 spotDirections[4];\n" +
"uniform vec3 spotColors[4];\n" +
"uniform vec3 spotParams[4];\n" +
"uniform vec4 materialBaseColor;\n" +
"uniform vec3 materialEmissiveColor;\n" +
"uniform float materialNormalScale;\n" +
"uniform float materialOcclusionStrength;\n" +
"uniform float materialMetallic;\n" +
"uniform float materialRoughness;\n" +
"uniform int materialAlphaMode;\n" +
"uniform float materialAlphaCutoff;\n" +
"uniform int materialHasBaseTexture;\n" +
"uniform int materialHasEmissiveTexture;\n" +
"uniform int materialHasMetallicRoughnessTexture;\n" +
"uniform int materialHasOcclusionTexture;\n" +
"uniform int materialHasNormalTexture;\n" +
"uniform int materialHasTangents;\n" +
"uniform int materialUnlit;\n" +
"uniform int materialReceiveShadows;\n" +
"uniform int shadowEnabled[4];\n" +
"uniform int shadowCascadeCounts[4];\n" +
"uniform sampler2D shadowMap0;\n" +
"uniform sampler2D shadowMap1;\n" +
"uniform sampler2D shadowMap2;\n" +
"uniform sampler2D shadowMap3;\n" +
"uniform mat4 shadowMatrices[16];\n" +
"uniform vec4 shadowRects[16];\n" +
"uniform vec2 shadowBounds[16];\n" +
"uniform float shadowDepthBiases[4];\n" +
"uniform float shadowNormalBiases[4];\n" +
"uniform vec2 directionalShadowTexelSize;\n" +
"uniform int spotShadowEnabled[4];\n" +
"uniform sampler2D spotShadowAtlas;\n" +
"uniform mat4 spotShadowMatrices[4];\n" +
"uniform vec4 spotShadowRects[4];\n" +
"uniform float spotShadowDepthBiases[4];\n" +
"uniform float spotShadowNormalBiases[4];\n" +
"uniform vec2 spotShadowTexelSize;\n" +
"uniform int pointShadowEnabled[8];\n" +
"uniform sampler2D pointShadowAtlas;\n" +
"uniform mat4 pointShadowMatrices[48];\n" +
"uniform vec4 pointShadowRects[48];\n" +
"uniform float pointShadowDepthBiases[8];\n" +
"uniform float pointShadowNormalBiases[8];\n" +
"uniform vec2 pointShadowTexelSize;\n" +
"uniform vec4 baseUvOffsetScale;\n" +
"uniform vec2 baseUvRotation;\n" +
"uniform int baseUvSet;\n" +
"uniform vec4 metallicRoughnessUvOffsetScale;\n" +
"uniform vec2 metallicRoughnessUvRotation;\n" +
"uniform int metallicRoughnessUvSet;\n" +
"uniform vec4 normalUvOffsetScale;\n" +
"uniform vec2 normalUvRotation;\n" +
"uniform int normalUvSet;\n" +
"uniform vec4 occlusionUvOffsetScale;\n" +
"uniform vec2 occlusionUvRotation;\n" +
"uniform int occlusionUvSet;\n" +
"uniform vec4 emissiveUvOffsetScale;\n" +
"uniform vec2 emissiveUvRotation;\n" +
"uniform int emissiveUvSet;\n" +
"out vec4 finalColor;\n" +
"float rangeAttenuation(float distanceToLight, float lightRange) {\n" +
" if (lightRange <= 0.000001) return 1.0;\n" +
" if (distanceToLight >= lightRange) return 0.0;\n" +
" float t = 1.0 - distanceToLight / lightRange;\n" +
" return t * t;\n" +
"}\n" +
"vec2 selectUv(int setIndex) {\n" +
" if (setIndex == 1) return fragTexCoord2;\n" +
" return fragTexCoord;\n" +
"}\n" +
"vec2 applyTextureTransform(vec2 uv, vec4 offsetScale, vec2 rotationSinCos) {\n" +
" vec2 scaled = uv * offsetScale.zw;\n" +
" return vec2(\n" +
" scaled.x * rotationSinCos.y - scaled.y * rotationSinCos.x + offsetScale.x,\n" +
" scaled.x * rotationSinCos.x + scaled.y * rotationSinCos.y + offsetScale.y\n" +
" );\n" +
"}\n" +
"float spotAttenuation(vec3 spotDirection, vec3 lightToFragment, vec3 params) {\n" +
" float inner = max(params.y, params.z);\n" +
" float outer = min(params.y, params.z);\n" +
" float cosTheta = dot(normalize(spotDirection), normalize(lightToFragment));\n" +
" float delta = inner - outer;\n" +
" if (cosTheta <= outer) return 0.0;\n" +
" if (cosTheta >= inner || abs(delta) <= 0.000001) return 1.0;\n" +
" return (cosTheta - outer) / delta;\n" +
"}\n" +
"mat3 cotangentFrame(vec3 normal, vec3 worldPosition, vec2 uv) {\n" +
" vec3 dp1 = dFdx(worldPosition);\n" +
" vec3 dp2 = dFdy(worldPosition);\n" +
" vec2 duv1 = dFdx(uv);\n" +
" vec2 duv2 = dFdy(uv);\n" +
" vec3 dp2perp = cross(dp2, normal);\n" +
" vec3 dp1perp = cross(normal, dp1);\n" +
" vec3 tangent = dp2perp * duv1.x + dp1perp * duv2.x;\n" +
" vec3 bitangent = dp2perp * duv1.y + dp1perp * duv2.y;\n" +
" float scale = max(dot(tangent, tangent), dot(bitangent, bitangent));\n" +
" if (scale <= 0.000001) return mat3(1.0);\n" +
" float invScale = inversesqrt(scale);\n" +
" return mat3(tangent * invScale, bitangent * invScale, normal);\n" +
"}\n" +
"vec3 applyNormalMap(vec3 normal, vec3 worldPosition, vec2 uv) {\n" +
" vec3 mapNormal = texture(texture2, uv).xyz * 2.0 - 1.0;\n" +
" mapNormal.xy *= materialNormalScale;\n" +
" mapNormal = normalize(mapNormal);\n" +
" mat3 tbn = cotangentFrame(normal, worldPosition, uv);\n" +
" if (materialHasTangents != 0) {\n" +
" vec3 tangent = normalize(fragTangent.xyz);\n" +
" vec3 bitangent = normalize(cross(normal, tangent) * fragTangent.w);\n" +
" if (dot(tangent, tangent) > 0.000001 && dot(bitangent, bitangent) > 0.000001) {\n" +
" tbn = mat3(tangent, bitangent, normal);\n" +
" }\n" +
" }\n" +
" return normalize(tbn * mapNormal);\n" +
"}\n" +
"void addLight(\n" +
" vec3 lightDir,\n" +
" vec3 lightColor,\n" +
" float attenuation,\n" +
" vec3 normal,\n" +
" vec3 viewDir,\n" +
" float shininess,\n" +
" float specularStrength,\n" +
" inout vec3 diffuseLight,\n" +
" inout vec3 specularLight\n" +
") {\n" +
" if (attenuation <= 0.0) return;\n" +
" float lambert = max(dot(normal, lightDir), 0.0);\n" +
" if (lambert <= 0.0) return;\n" +
" diffuseLight += lightColor * (lambert * attenuation);\n" +
" vec3 halfway = normalize(lightDir + viewDir);\n" +
" float spec = pow(max(dot(normal, halfway), 0.0), shininess) * specularStrength;\n" +
" specularLight += lightColor * (spec * attenuation);\n" +
"}\n" +
"float sampleShadowMapDepth(int index, vec2 uv) {\n" +
" if (index == 0) return texture(shadowMap0, uv).r;\n" +
" if (index == 1) return texture(shadowMap1, uv).r;\n" +
" if (index == 2) return texture(shadowMap2, uv).r;\n" +
" if (index == 3) return texture(shadowMap3, uv).r;\n" +
" return 1.0;\n" +
"}\n" +
"float sampleShadowAtlasDepth(sampler2D atlasTexture, vec2 uv, vec4 atlasRect) {\n" +
" return texture(atlasTexture, atlasRect.xy + uv * atlasRect.zw).r;\n" +
"}\n" +
"float sampleDirectionalShadowCascade(int lightIndex, int cascadeIndex, vec3 normal, vec3 lightDir) {\n" +
" int matrixIndex = lightIndex * 4 + cascadeIndex;\n" +
" vec4 atlasRect = shadowRects[matrixIndex];\n" +
" if (atlasRect.z <= 0.0 || atlasRect.w <= 0.0) return 1.0;\n" +
" vec4 shadowClip = shadowMatrices[matrixIndex] * vec4(fragPosition, 1.0);\n" +
" if (abs(shadowClip.w) <= 0.00001) return 1.0;\n" +
" vec3 shadowNdc = shadowClip.xyz / shadowClip.w;\n" +
" vec2 shadowUv = shadowNdc.xy * 0.5 + 0.5;\n" +
" float shadowDepth = shadowNdc.z * 0.5 + 0.5;\n" +
" if (shadowDepth <= 0.0 || shadowDepth >= 1.0) return 1.0;\n" +
" if (shadowUv.x <= 0.0 || shadowUv.x >= 1.0 || shadowUv.y <= 0.0 || shadowUv.y >= 1.0) return 1.0;\n" +
" float texel = max(directionalShadowTexelSize.x, directionalShadowTexelSize.y);\n" +
" float ndotl = max(dot(normal, lightDir), 0.0);\n" +
" float bias = max(0.0, shadowDepthBiases[lightIndex]) + max(0.0, shadowNormalBiases[lightIndex]) * texel * (1.0 - ndotl);\n" +
" float filterRadius = 1.25;\n" +
" float lit = 0.0;\n" +
" float totalWeight = 0.0;\n" +
" for (int x = -1; x <= 1; x++) {\n" +
" for (int y = -1; y <= 1; y++) {\n" +
" float weight = 1.0 / (1.0 + abs(float(x)) + abs(float(y)));\n" +
" vec2 sampleUv = shadowUv + vec2(float(x), float(y)) * directionalShadowTexelSize * filterRadius;\n" +
" float closest = sampleShadowMapDepth(lightIndex, clamp(atlasRect.xy + sampleUv * atlasRect.zw, vec2(0.0), vec2(1.0)));\n" +
" lit += (shadowDepth - bias <= closest ? 1.0 : 0.0) * weight;\n" +
" totalWeight += weight;\n" +
" }\n" +
" }\n" +
" return totalWeight <= 0.00001 ? 1.0 : lit / totalWeight;\n" +
"}\n" +
"float sampleDirectionalShadow(int index, vec3 normal, vec3 lightDir) {\n" +
" if (materialReceiveShadows == 0 || index < 0 || index >= 4 || shadowEnabled[index] == 0) return 1.0;\n" +
" int cascadeCount = shadowCascadeCounts[index];\n" +
" if (cascadeCount <= 0) return 1.0;\n" +
" float viewDepth = dot(fragPosition - viewPosition, viewForward);\n" +
" int baseIndex = index * 4;\n" +
" float firstNear = shadowBounds[baseIndex].x;\n" +
" float lastFar = shadowBounds[baseIndex + cascadeCount - 1].y;\n" +
" if (viewDepth < firstNear || viewDepth > lastFar) return 1.0;\n" +
" int selected = cascadeCount - 1;\n" +
" for (int i = 0; i < 4; i++) {\n" +
" if (i >= cascadeCount) break;\n" +
" if (viewDepth <= shadowBounds[baseIndex + i].y) {\n" +
" selected = i;\n" +
" break;\n" +
" }\n" +
" }\n" +
" float shadow = sampleDirectionalShadowCascade(index, selected, normal, lightDir);\n" +
" if (selected > 0) {\n" +
" int previous = selected - 1;\n" +
" float overlapNear = shadowBounds[baseIndex + selected].x;\n" +
" float overlapFar = shadowBounds[baseIndex + previous].y;\n" +
" if (overlapNear < overlapFar && viewDepth >= overlapNear && viewDepth <= overlapFar) {\n" +
" float blend = clamp((viewDepth - overlapNear) / (overlapFar - overlapNear), 0.0, 1.0);\n" +
" shadow = mix(sampleDirectionalShadowCascade(index, previous, normal, lightDir), shadow, blend);\n" +
" }\n" +
" }\n" +
" return shadow;\n" +
"}\n" +
"float sampleSpotShadow(int index, vec3 normal, vec3 lightDir) {\n" +
" if (materialReceiveShadows == 0 || index < 0 || index >= 4 || spotShadowEnabled[index] == 0) return 1.0;\n" +
" vec4 shadowClip = spotShadowMatrices[index] * vec4(fragPosition, 1.0);\n" +
" if (abs(shadowClip.w) <= 0.00001) return 1.0;\n" +
" vec3 shadowNdc = shadowClip.xyz / shadowClip.w;\n" +
" vec2 shadowUv = shadowNdc.xy * 0.5 + 0.5;\n" +
" float shadowDepth = shadowNdc.z * 0.5 + 0.5;\n" +
" if (shadowDepth <= 0.0 || shadowDepth >= 1.0) return 1.0;\n" +
" if (shadowUv.x <= 0.0 || shadowUv.x >= 1.0 || shadowUv.y <= 0.0 || shadowUv.y >= 1.0) return 1.0;\n" +
" float texel = max(spotShadowTexelSize.x, spotShadowTexelSize.y);\n" +
" float ndotl = max(dot(normal, lightDir), 0.0);\n" +
" float bias = max(0.0, spotShadowDepthBiases[index]) + max(0.0, spotShadowNormalBiases[index]) * texel * (1.0 - ndotl);\n" +
" float filterRadius = 1.25;\n" +
" float lit = 0.0;\n" +
" float totalWeight = 0.0;\n" +
" for (int x = -1; x <= 1; x++) {\n" +
" for (int y = -1; y <= 1; y++) {\n" +
" float weight = 1.0 / (1.0 + abs(float(x)) + abs(float(y)));\n" +
" vec2 sampleUv = shadowUv + vec2(float(x), float(y)) * spotShadowTexelSize * filterRadius;\n" +
" float closest = sampleShadowAtlasDepth(spotShadowAtlas, clamp(sampleUv, vec2(0.0), vec2(1.0)), spotShadowRects[index]);\n" +
" lit += (shadowDepth - bias <= closest ? 1.0 : 0.0) * weight;\n" +
" totalWeight += weight;\n" +
" }\n" +
" }\n" +
" return totalWeight <= 0.00001 ? 1.0 : lit / totalWeight;\n" +
"}\n" +
"int selectPointShadowFace(vec3 dir) {\n" +
" vec3 absDir = abs(dir);\n" +
" if (absDir.x >= absDir.y && absDir.x >= absDir.z) return dir.x >= 0.0 ? 0 : 1;\n" +
" if (absDir.y >= absDir.x && absDir.y >= absDir.z) return dir.y >= 0.0 ? 2 : 3;\n" +
" return dir.z >= 0.0 ? 4 : 5;\n" +
"}\n" +
"float samplePointShadow(int index, vec3 normal, vec3 lightDir, vec3 lightToFragment) {\n" +
" if (materialReceiveShadows == 0 || index < 0 || index >= 8 || pointShadowEnabled[index] == 0) return 1.0;\n" +
" int face = selectPointShadowFace(lightToFragment);\n" +
" int matrixIndex = index * 6 + face;\n" +
" vec4 shadowClip = pointShadowMatrices[matrixIndex] * vec4(fragPosition, 1.0);\n" +
" if (abs(shadowClip.w) <= 0.00001) return 1.0;\n" +
" vec3 shadowNdc = shadowClip.xyz / shadowClip.w;\n" +
" vec2 shadowUv = shadowNdc.xy * 0.5 + 0.5;\n" +
" float shadowDepth = shadowNdc.z * 0.5 + 0.5;\n" +
" if (shadowDepth <= 0.0 || shadowDepth >= 1.0) return 1.0;\n" +
" if (shadowUv.x <= 0.0 || shadowUv.x >= 1.0 || shadowUv.y <= 0.0 || shadowUv.y >= 1.0) return 1.0;\n" +
" float texel = max(pointShadowTexelSize.x, pointShadowTexelSize.y);\n" +
" float ndotl = max(dot(normal, lightDir), 0.0);\n" +
" float bias = max(0.0, pointShadowDepthBiases[index]) + max(0.0, pointShadowNormalBiases[index]) * texel * (1.0 - ndotl);\n" +
" float filterRadius = 1.0;\n" +
" float lit = 0.0;\n" +
" float totalWeight = 0.0;\n" +
" for (int x = -1; x <= 1; x++) {\n" +
" for (int y = -1; y <= 1; y++) {\n" +
" float weight = 1.0 / (1.0 + abs(float(x)) + abs(float(y)));\n" +
" vec2 sampleUv = shadowUv + vec2(float(x), float(y)) * pointShadowTexelSize * filterRadius;\n" +
" float closest = sampleShadowAtlasDepth(pointShadowAtlas, clamp(sampleUv, vec2(0.0), vec2(1.0)), pointShadowRects[matrixIndex]);\n" +
" lit += (shadowDepth - bias <= closest ? 1.0 : 0.0) * weight;\n" +
" totalWeight += weight;\n" +
" }\n" +
" }\n" +
" return totalWeight <= 0.00001 ? 1.0 : lit / totalWeight;\n" +
"}\n" +
"void main() {\n" +
" vec2 baseUv = applyTextureTransform(selectUv(baseUvSet), baseUvOffsetScale, baseUvRotation);\n" +
" vec2 metallicRoughnessUv = applyTextureTransform(selectUv(metallicRoughnessUvSet), metallicRoughnessUvOffsetScale, metallicRoughnessUvRotation);\n" +
" vec2 normalUv = applyTextureTransform(selectUv(normalUvSet), normalUvOffsetScale, normalUvRotation);\n" +
" vec2 occlusionUv = applyTextureTransform(selectUv(occlusionUvSet), occlusionUvOffsetScale, occlusionUvRotation);\n" +
" vec2 emissiveUv = applyTextureTransform(selectUv(emissiveUvSet), emissiveUvOffsetScale, emissiveUvRotation);\n" +
" vec4 albedoSample = vec4(1.0);\n" +
" if (materialHasBaseTexture != 0) albedoSample = texture(texture0, baseUv);\n" +
" vec4 albedo = albedoSample * fragVertexColor * materialBaseColor;\n" +
" if (materialAlphaMode == 0) {\n" +
" albedo.a = 1.0;\n" +
" } else if (materialAlphaMode == 1) {\n" +
" if (albedo.a < materialAlphaCutoff) discard;\n" +
" albedo.a = 1.0;\n" +
" } else if (albedo.a <= 0.0001) discard;\n" +
" if (materialUnlit != 0) {\n" +
" finalColor = vec4(albedo.rgb, albedo.a);\n" +
" return;\n" +
" }\n" +
" vec4 metallicRoughnessSample = vec4(1.0);\n" +
" if (materialHasMetallicRoughnessTexture != 0) {\n" +
" metallicRoughnessSample = texture(texture1, metallicRoughnessUv);\n" +
" }\n" +
" vec3 normal = normalize(fragNormal);\n" +
" if (length(normal) <= 0.00001) normal = vec3(0.0, 1.0, 0.0);\n" +
" vec3 geometricNormal = normal;\n" +
" if (materialHasNormalTexture != 0) normal = applyNormalMap(normal, fragPosition, normalUv);\n" +
" vec3 viewDir = normalize(viewPosition - fragPosition);\n" +
" if (length(viewDir) <= 0.00001) viewDir = vec3(0.0, 0.0, 1.0);\n" +
" float roughness = clamp(materialRoughness * metallicRoughnessSample.g, 0.089, 1.0);\n" +
" float metallic = clamp(materialMetallic * metallicRoughnessSample.b, 0.0, 1.0);\n" +
" float shininess = mix(96.0, 8.0, roughness);\n" +
" float specularStrength = mix(0.04, 1.0, metallic);\n" +
" vec3 diffuseLight = ambientLight;\n" +
" vec3 specularLight = vec3(0.0);\n" +
" for (int i = 0; i < directionalCount; i++) {\n" +
" vec3 lightDir = normalize(-directionalDirections[i]);\n" +
" float attenuation = sampleDirectionalShadow(i, geometricNormal, lightDir);\n" +
" addLight(lightDir, directionalColors[i], attenuation, normal, viewDir, shininess, specularStrength, diffuseLight, specularLight);\n" +
" }\n" +
" for (int i = 0; i < pointCount; i++) {\n" +
" vec3 toLight = pointPositions[i] - fragPosition;\n" +
" float distanceToLight = length(toLight);\n" +
" if (distanceToLight <= 0.00001) continue;\n" +
" vec3 lightDir = toLight / distanceToLight;\n" +
" float attenuation = rangeAttenuation(distanceToLight, pointRanges[i]) * samplePointShadow(i, geometricNormal, lightDir, fragPosition - pointPositions[i]);\n" +
" addLight(lightDir, pointColors[i], attenuation, normal, viewDir, shininess, specularStrength, diffuseLight, specularLight);\n" +
" }\n" +
" for (int i = 0; i < spotCount; i++) {\n" +
" vec3 toLight = spotPositions[i] - fragPosition;\n" +
" float distanceToLight = length(toLight);\n" +
" if (distanceToLight <= 0.00001) continue;\n" +
" vec3 lightDir = toLight / distanceToLight;\n" +
" float attenuation = rangeAttenuation(distanceToLight, spotParams[i].x);\n" +
" float cone = spotAttenuation(spotDirections[i], -lightDir, spotParams[i]);\n" +
" addLight(lightDir, spotColors[i], attenuation * cone * sampleSpotShadow(i, geometricNormal, lightDir), normal, viewDir, shininess, specularStrength, diffuseLight, specularLight);\n" +
" }\n" +
" if (directionalCount == 0 && pointCount == 0 && spotCount == 0 && dot(ambientLight, ambientLight) <= 0.000001) {\n" +
" diffuseLight = vec3(1.0);\n" +
" }\n" +
" float occlusion = 1.0;\n" +
" if (materialHasOcclusionTexture != 0) {\n" +
" occlusion = mix(1.0, texture(texture4, occlusionUv).r, clamp(materialOcclusionStrength, 0.0, 1.0));\n" +
" }\n" +
" vec3 emissive = materialEmissiveColor;\n" +
" if (materialHasEmissiveTexture != 0) {\n" +
" emissive = texture(texture5, emissiveUv).rgb * materialEmissiveColor;\n" +
" }\n" +
" vec3 litColor = albedo.rgb * diffuseLight * occlusion + specularLight * occlusion + emissive;\n" +
" finalColor = vec4(litColor, albedo.a);\n" +
"}\n"
///|
fn default_font_codepoints() -> Array[Int] {
let codepoints : Array[Int] = []
// Basic printable ASCII.
for cp in 32..<127 {
codepoints.push(cp)
}
// Latin-1 supplement (includes multiplication sign U+00D7).
for cp in 160..<256 {
codepoints.push(cp)
}
codepoints
}
///|
fn default_embedded_asset_lookup(_path : String) -> Bytes? {
None
}
///|
fn read_float(bytes : Bytes, off : Int) -> Float {
let bits = bytes[off].to_int() |
(bytes[off + 1].to_int() << 8) |
(bytes[off + 2].to_int() << 16) |
(bytes[off + 3].to_int() << 24)
Float::reinterpret_from_int(bits)
}
///|
fn clamp_u8(value : Int) -> Int {
if value < 0 {
0
} else if value > 255 {
255
} else {
value
}
}
///|
fn to_float(value : Double) -> Float {
Float::from_double(value)
}
///|
fn clamp_playback_speed(speed : Double) -> Double {
if speed <= 0.0 {
0.01
} else {
speed
}
}
///|
fn to_ray_vector2(x : Double, y : Double) -> @raylib.Vector2 {
@raylib.Vector2::new(to_float(x), to_float(y))
}
///|
fn to_ray_vector3_smath(value : @smath.Vec3) -> @raylib.Vector3 {
@raylib.Vector3::new(to_float(value.x), to_float(value.y), to_float(value.z))
}
///|
fn to_ray_rect(
x : Double,
y : Double,
width : Double,
height : Double,
) -> @raylib.Rectangle {
@raylib.Rectangle::new(
to_float(x),
to_float(y),
to_float(width),
to_float(height),
)
}
///|
fn clip_rect_intersection(lhs : @smath.Rect, rhs : @smath.Rect) -> @smath.Rect? {
let left = @cmp.maximum(lhs.position[X], rhs.position[X])
let top = @cmp.maximum(lhs.position[Y], rhs.position[Y])
let right = @cmp.minimum(
lhs.position[X] + lhs.size[X],
rhs.position[X] + rhs.size[X],
)
let bottom = @cmp.minimum(
lhs.position[Y] + lhs.size[Y],
rhs.position[Y] + rhs.size[Y],
)
if right <= left || bottom <= top {
None
} else {
Some({
position: @smath.Vec2D(left, top),
size: @smath.Vec2D(right - left, bottom - top),
})
}
}
///|
fn activate_clip_rect(rect : @smath.Rect) -> Unit {
@raylib.begin_scissor_mode(
rect.position[X].to_int(),
rect.position[Y].to_int(),
rect.size[X].to_int(),
rect.size[Y].to_int(),
)
}
///|
fn refresh_clip_stack() -> Unit {
@raylib.end_scissor_mode()
match clip_stack.last() {
Some(rect) => activate_clip_rect(rect)
None => ()
}
}
///|
fn vector2_x(value : @raylib.Vector2) -> Double {
read_float(value.to_bytes(), 0).to_double()
}
///|
fn vector2_y(value : @raylib.Vector2) -> Double {
read_float(value.to_bytes(), 4).to_double()
}
///|
fn to_selene_vec2(value : @raylib.Vector2) -> @smath.Vec2D {
@smath.Vec2D(vector2_x(value), vector2_y(value))
}
///|
fn to_ray_color(color : @render.Color) -> @raylib.Color {
@raylib.Color::new(
color.r.reinterpret_as_int(),
color.g.reinterpret_as_int(),
color.b.reinterpret_as_int(),
clamp_u8((color.a * 255.0).to_int()),
)
}
///|
fn to_ray_color_bytes(color : @render.Color) -> (Byte, Byte, Byte, Byte) {
(
color.r.reinterpret_as_int().to_byte(),
color.g.reinterpret_as_int().to_byte(),
color.b.reinterpret_as_int().to_byte(),
clamp_u8((color.a * 255.0).to_int()).to_byte(),
)
}
///|
fn color_to_vec3(color : @render.Color) -> @smath.Vec3 {
@smath.Vec3::new(
color.r.to_double() / 255.0,
color.g.to_double() / 255.0,
color.b.to_double() / 255.0,
)
}
///|
fn normalize_or(value : @smath.Vec3, fallback : @smath.Vec3) -> @smath.Vec3 {
if value.length_squared() <= 0.0000001 {
fallback
} else {
value.normalize()
}
}
///|
fn light_rgb(color : @render.Color, intensity : Double) -> @smath.Vec3 {
color_to_vec3(color).scalar_mul(@cmp.maximum(0.0, intensity))
}
///|
fn uniform_int(value : Int) -> @raylib.ShaderUniformData {
@raylib.ShaderUniformData::Int(value)
}
///|
fn uniform_texture_unit(value : Int) -> @raylib.ShaderUniformData {
// NOTE: Keep texture samplers on Int uniforms for raylib 0.3.1 compatibility.
// Some drivers/backend paths interpret SHADER_UNIFORM_SAMPLER2D inconsistently.
@raylib.ShaderUniformData::Int(value)
}
///|
fn uniform_float(value : Double) -> @raylib.ShaderUniformData {
@raylib.ShaderUniformData::Float(to_float(value))
}
///|
fn uniform_vec2(value : @smath.Vec2D) -> @raylib.ShaderUniformData {
@raylib.ShaderUniformData::Vec2(
@raylib.Vector2::new(to_float(value[X]), to_float(value[Y])),
)
}
///|
fn uniform_vec3(value : @smath.Vec3) -> @raylib.ShaderUniformData {
@raylib.ShaderUniformData::Vec3(
@raylib.Vector3::new(
to_float(value.x),
to_float(value.y),
to_float(value.z),
),
)
}
///|
fn material_texture_slot_bindings() -> Array[MaterialTextureSlotBinding] {
[
{
shader_loc: @raylib.ShaderLocMapAlbedo,
uniform_name: "texture0",
slot: 0,
material_map: @raylib.MaterialMapAlbedo,
},
{
shader_loc: @raylib.ShaderLocMapMetalness,
uniform_name: "texture1",
slot: 1,
material_map: @raylib.MaterialMapMetalness,
},
{
shader_loc: @raylib.ShaderLocMapNormal,
uniform_name: "texture2",
slot: 2,
material_map: @raylib.MaterialMapNormal,
},
{
shader_loc: @raylib.ShaderLocMapOcclusion,
uniform_name: "texture4",
slot: 4,
material_map: @raylib.MaterialMapOcclusion,
},
{
shader_loc: @raylib.ShaderLocMapEmission,
uniform_name: "texture5",
slot: 5,
material_map: @raylib.MaterialMapEmission,
},
]
}
///|
fn uniform_color4(color : @render.Color) -> @raylib.ShaderUniformData {
@raylib.ShaderUniformData::Vec4(
@raylib.Vector4::new(
to_float(color.r.to_double() / 255.0),
to_float(color.g.to_double() / 255.0),
to_float(color.b.to_double() / 255.0),
to_float(color.a),
),
)
}
///|
fn reset_material_from_material(
target : @raylib.Material,
source : @raylib.Material,
) -> Unit {
target.set_shader(source.shader())
let target_maps = target.maps()
let source_maps = source.maps()
let count = @cmp.minimum(target_maps.count(), source_maps.count())
for i in 0.. (Double, Double, Double, Double) {
(
transform.offset[X],
transform.offset[Y],
transform.scale[X],
transform.scale[Y],
)
}
///|
fn texture_transform_rotation_sin_cos(
transform : @render3d_types.TextureTransform3D,
) -> @smath.Vec2D {
@smath.Vec2D(@cmath.sin(transform.rotation), @cmath.cos(transform.rotation))
}
///|
fn uniform_texture_transform_offset_scale(
transform : @render3d_types.TextureTransform3D,
) -> @raylib.ShaderUniformData {
let (offset_x, offset_y, scale_x, scale_y) = texture_transform_offset_scale(
transform,
)
@raylib.ShaderUniformData::Vec4(
@raylib.Vector4::new(
to_float(offset_x),
to_float(offset_y),
to_float(scale_x),
to_float(scale_y),
),
)
}
///|
fn uniform_texture_transform_rotation(
transform : @render3d_types.TextureTransform3D,
) -> @raylib.ShaderUniformData {
uniform_vec2(texture_transform_rotation_sin_cos(transform))
}
///|
fn to_ray_vec3_array(
values : Array[@smath.Vec3],
count : Int,
) -> Array[@raylib.Vector3] {
let out : Array[@raylib.Vector3] = []
for index in 0.. Array[Float] {
let out : Array[Float] = []
for index in 0.. Unit {
if loc == -1 {
return
}
shader.set_value(loc, value)
}
///|
fn set_shader_uniform_v(
shader : @raylib.Shader,
loc : Int,
data : @raylib.ShaderUniformDataV,
) -> Unit {
if loc == -1 {
return
}
shader.set_value_v(loc, data)
}
///|
fn configure_lighting_shader_locations(shader : @raylib.Shader) -> Unit {
shader.set_locs(
@raylib.ShaderLocVertexPosition,
@raylib.get_shader_location_attrib(shader, "vertexPosition"),
)
shader.set_locs(
@raylib.ShaderLocVertexTexcoord01,
@raylib.get_shader_location_attrib(shader, "vertexTexCoord"),
)
shader.set_locs(
@raylib.ShaderLocVertexTexcoord02,
@raylib.get_shader_location_attrib(shader, "vertexTexCoord2"),
)
shader.set_locs(
@raylib.ShaderLocVertexNormal,
@raylib.get_shader_location_attrib(shader, "vertexNormal"),
)
shader.set_locs(
@raylib.ShaderLocVertexTangent,
@raylib.get_shader_location_attrib(shader, "vertexTangent"),
)
shader.set_locs(
@raylib.ShaderLocVertexColor,
@raylib.get_shader_location_attrib(shader, "vertexColor"),
)
shader.set_locs(
@raylib.ShaderLocMatrixMvp,
@raylib.get_shader_location(shader, "mvp"),
)
shader.set_locs(
@raylib.ShaderLocMatrixView,
@raylib.get_shader_location(shader, "matView"),
)
shader.set_locs(
@raylib.ShaderLocMatrixProjection,
@raylib.get_shader_location(shader, "matProjection"),
)
shader.set_locs(
@raylib.ShaderLocMatrixModel,
@raylib.get_shader_location(shader, "matModel"),
)
shader.set_locs(
@raylib.ShaderLocMatrixNormal,
@raylib.get_shader_location(shader, "matNormal"),
)
for binding in material_texture_slot_bindings() {
shader.set_locs(
binding.shader_loc,
@raylib.get_shader_location(shader, binding.uniform_name),
)
}
}
///|
fn create_lighting_shader_state() -> LightingShaderState {
let shader = @raylib.load_shader_from_memory(
raylib_lighting_vertex_shader, raylib_lighting_fragment_shader,
)
configure_lighting_shader_locations(shader)
let shadow_enabled_locs : Array[Int] = []
let shadow_cascade_count_locs : Array[Int] = []
let shadow_matrix_locs : Array[Int] = []
let shadow_rect_locs : Array[Int] = []
let shadow_bound_locs : Array[Int] = []
let shadow_depth_bias_locs : Array[Int] = []
let shadow_normal_bias_locs : Array[Int] = []
let shadow_sampler_locs : Array[Int] = []
let spot_shadow_enabled_locs : Array[Int] = []
let spot_shadow_matrix_locs : Array[Int] = []
let spot_shadow_rect_locs : Array[Int] = []
let spot_shadow_depth_bias_locs : Array[Int] = []
let spot_shadow_normal_bias_locs : Array[Int] = []
let point_shadow_enabled_locs : Array[Int] = []
let point_shadow_depth_bias_locs : Array[Int] = []
let point_shadow_normal_bias_locs : Array[Int] = []
let point_shadow_matrix_locs : Array[Int] = []
let point_shadow_rect_locs : Array[Int] = []
for index in 0.. LightingShaderState {
if backend.lighting_shader is Some(state) {
return state
}
let state = create_lighting_shader_state()
backend.lighting_shader = Some(state)
state
}
///|
fn white_render_color() -> @render.Color {
{ r: 255U, g: 255U, b: 255U, a: 1.0 }
}
///|
const INTERNAL_WHITE_TEXTURE_KEY : String = "__selene_internal:white_1x1"
///|
fn ensure_texture_mipmaps(texture : @raylib.Texture) -> Unit {
if texture.mipmaps() < 2 {
texture.gen_mipmaps()
}
}
///|
fn get_solid_texture(key : String, color : @render.Color) -> @raylib.Texture {
if backend.textures.get(key) is Some(texture) {
return texture
}
let image = @raylib.gen_image_color(1, 1, to_ray_color(color))
let texture = @raylib.load_texture_from_image(image)
@raylib.unload_image(image)
ensure_texture_mipmaps(texture)
@raylib.set_texture_filter(texture, @raylib.TextureFilterPoint)
backend.textures.set(key, texture)
texture
}
///|
fn get_white_texture() -> @raylib.Texture {
get_solid_texture(INTERNAL_WHITE_TEXTURE_KEY, white_render_color())
}
///|
fn fallback_lighting_texture_bundle() -> MaterialTextureBundle3D {
let default_transform = @render3d_types.default_texture_transform3d()
{
base_texture: get_white_texture(),
emissive_texture: get_white_texture(),
metallic_roughness_texture: get_white_texture(),
occlusion_texture: get_white_texture(),
normal_texture: null_texture(),
has_base_texture: false,
has_emissive_texture: false,
has_metallic_roughness_texture: false,
has_occlusion_texture: false,
has_normal_texture: false,
base_transform: default_transform,
emissive_transform: default_transform,
metallic_roughness_transform: default_transform,
occlusion_transform: default_transform,
normal_transform: default_transform,
base_texcoord_set: 0,
emissive_texcoord_set: 0,
metallic_roughness_texcoord_set: 0,
occlusion_texcoord_set: 0,
normal_texcoord_set: 0,
}
}
///|
fn get_lit_material(textured : Bool) -> @raylib.Material {
let existing = if textured {
backend.lit_textured_material
} else {
backend.lit_material
}
if existing is Some(material) {
return material
}
let material = @raylib.Material::default()
@raylib.set_material_shader(material, get_lighting_shader_state().shader)
@raylib.set_material_map_color(
material,
@raylib.MaterialMapAlbedo,
to_ray_color(white_render_color()),
)
if textured {
backend.lit_textured_material = Some(material)
} else {
backend.lit_material = Some(material)
}
material
}
///|
fn bind_shadow_textures(shadow_state : FrameShadowState?) -> Unit {
guard shadow_state is Some(frame_shadow) else { return }
for index in 0.. Unit {
guard shadow_state is Some(frame_shadow) else { return }
for index in 0.. Unit {
set_shader_uniform(
shader,
offset_scale_loc,
uniform_texture_transform_offset_scale(transform),
)
set_shader_uniform(
shader,
rotation_loc,
uniform_texture_transform_rotation(transform),
)
}
///|
fn upload_lighting_uniforms(
frame : @render3d_types.RenderFrame3D,
camera_position : @smath.Vec3,
material : @render3d_types.StandardMaterial3D,
texture_bundle : MaterialTextureBundle3D?,
mesh_has_tangents : Bool,
receive_shadows : Bool,
shadow_state : FrameShadowState?,
) -> LightingShaderState {
let state = get_lighting_shader_state()
let shader = state.shader
let default_shadow_rect : ShadowAtlasRect = {
offset: @smath.Vec2D(0.0, 0.0),
scale: @smath.Vec2D(0.0, 0.0),
}
let ambient = frame.ambient_light
.map(fn(light) { light_rgb(light.color, light.intensity) })
.unwrap_or(@smath.Vec3::zero())
set_shader_uniform(shader, state.ambient_light_loc, uniform_vec3(ambient))
set_shader_uniform(
shader,
state.view_position_loc,
uniform_vec3(camera_position),
)
let camera_forward = frame.camera.map_or(@smath.Vec3::new(0.0, 0.0, -1.0), fn(
active_camera,
) {
normalize_or(
active_camera.target - active_camera.position,
@smath.Vec3::new(0.0, 0.0, -1.0),
)
})
set_shader_uniform(
shader,
state.view_forward_loc,
uniform_vec3(camera_forward),
)
let directional_count = @cmp.minimum(
frame.directional_lights.length(),
MAX_DIRECTIONAL_LIGHTS,
)
let directional_directions : Array[@smath.Vec3] = []
let directional_colors : Array[@smath.Vec3] = []
for index in 0.. frame_shadow.directional_shadows.get(index)
None => None
}
set_shader_uniform(
shader,
state.shadow_enabled_locs[index],
uniform_int(if shadow is Some(_) { 1 } else { 0 }),
)
set_shader_uniform(
shader,
state.shadow_cascade_count_locs[index],
uniform_int(shadow.map_or(0, fn(value) { value.cascades.length() })),
)
set_shader_uniform(
shader,
state.shadow_depth_bias_locs[index],
uniform_float(shadow.map_or(0.0, fn(value) { value.depth_bias })),
)
set_shader_uniform(
shader,
state.shadow_normal_bias_locs[index],
uniform_float(shadow.map_or(0.0, fn(value) { value.normal_bias })),
)
let base_index = index * MAX_DIRECTIONAL_CASCADES
for cascade_index in 0.. frame_shadow.spot_shadows.get(index)
None => None
}
set_shader_uniform(
shader,
state.spot_shadow_enabled_locs[index],
uniform_int(if shadow is Some(_) { 1 } else { 0 }),
)
@raylib.set_shader_value_matrix(
shader,
state.spot_shadow_matrix_locs[index],
shadow.map_or(@raylib.Matrix::identity(), fn(value) {
value.light_view_projection
}),
)
set_shader_uniform(
shader,
state.spot_shadow_rect_locs[index],
uniform_shadow_atlas_rect(
shadow.map_or(default_shadow_rect, fn(value) { value.atlas_rect }),
),
)
set_shader_uniform(
shader,
state.spot_shadow_depth_bias_locs[index],
uniform_float(shadow.map_or(0.0, fn(value) { value.depth_bias })),
)
set_shader_uniform(
shader,
state.spot_shadow_normal_bias_locs[index],
uniform_float(shadow.map_or(0.0, fn(value) { value.normal_bias })),
)
}
set_shader_uniform(
shader,
state.spot_shadow_texel_size_loc,
uniform_vec2(
shadow_state.map_or(@smath.Vec2D(0.0, 0.0), fn(value) {
value.spot_texel_size
}),
),
)
for index in 0.. frame_shadow.point_shadows.get(index)
None => None
}
set_shader_uniform(
shader,
state.point_shadow_enabled_locs[index],
uniform_int(if shadow is Some(_) { 1 } else { 0 }),
)
set_shader_uniform(
shader,
state.point_shadow_depth_bias_locs[index],
uniform_float(shadow.map_or(0.0, fn(value) { value.depth_bias })),
)
set_shader_uniform(
shader,
state.point_shadow_normal_bias_locs[index],
uniform_float(shadow.map_or(0.0, fn(value) { value.normal_bias })),
)
for face_index in 0.. Int {
match mode {
Opaque => 0
Mask => 1
Blend => 2
}
}
///|
fn to_ray_camera3d(camera : @render3d_types.FrameCamera3D) -> @raylib.Camera3D {
@raylib.Camera3D::new(
to_ray_vector3_smath(camera.position),
to_ray_vector3_smath(camera.target),
to_ray_vector3_smath(camera.up),
to_float(
match camera.projection {
Perspective => camera.fov_y
Orthographic =>
camera.orthographic_size.map_or(10.0, fn(size) { size[Y] })
},
),
match camera.projection {
Perspective => @raylib.CameraPerspective
Orthographic => @raylib.CameraOrthographic
},
)
}
///|
fn configured_filter() -> Int {
if backend.image_smooth {
@raylib.TextureFilterBilinear
} else {
@raylib.TextureFilterPoint
}
}
///|
fn file_extension(path : String) -> String {
if path.to_lower().has_prefix("data:") {
return data_uri_extension(path)
}
let slash_idx = if path.rev_find("/") is Some(i) { i } else { -1 }
let backslash_idx = if path.rev_find("\\") is Some(i) { i } else { -1 }
let last_separator = if slash_idx > backslash_idx {
slash_idx
} else {
backslash_idx
}
if path.rev_find(".") is Some(dot_idx) && dot_idx > last_separator {
path.unsafe_substring(start=dot_idx, end=path.length())
} else {
".bin"
}
}
///|
fn data_uri_extension(uri : String) -> String {
guard uri.find(",") is Some(comma_idx) else { return ".bin" }
let meta = uri.unsafe_substring(start=0, end=comma_idx).to_lower()
if meta.contains("image/png") {
".png"
} else if meta.contains("image/jpeg") {
".jpg"
} else if meta.contains("image/webp") {
".webp"
} else if meta.contains("image/bmp") {
".bmp"
} else if meta.contains("image/tga") {
".tga"
} else {
".bin"
}
}
///|
fn decode_data_uri_base64(uri : String) -> Bytes? {
guard uri.find(",") is Some(comma_idx) else { return None }
let meta = uri.unsafe_substring(start=0, end=comma_idx).to_lower()
if !meta.has_suffix(";base64") {
return None
}
let payload = uri.unsafe_substring(start=comma_idx + 1, end=uri.length())
Some(@base64.decode_lossy(payload.view()))
}
///|
fn get_embedded_asset(path : String) -> Bytes? {
if backend.embedded_asset_cache.get(path) is Some(cached) {
return Some(cached)
}
if (backend.embedded_asset_lookup)(path) is Some(bytes) {
backend.embedded_asset_cache.set(path, bytes)
Some(bytes)
} else {
None
}
}
///|
fn warn_texture_issue(key : String, message : String) -> Unit {
if backend.texture_warning_keys.contains(key) {
return
}
backend.texture_warning_keys.add(key)
println("[selene-raylib][texture-warning] \{message}")
}
///|
fn get_or_create_primitive_mesh(
key : String,
create : () -> @raylib.Mesh,
) -> @raylib.Mesh {
if backend.primitive_meshes.get(key) is Some(mesh) {
return mesh
}
let mesh = create()
backend.primitive_meshes.set(key, mesh)
mesh
}
///|
fn triangle_mesh_cache_prefix(
mesh_handle : @render3d_types.MeshHandle,
) -> String {
"triangle_mesh:\{mesh_handle}:"
}
///|
fn triangle_mesh_cache_key(mesh_handle : @render3d_types.MeshHandle) -> String {
"\{triangle_mesh_cache_prefix(mesh_handle)}mesh"
}
///|
fn clear_triangle_mesh_cache_for_handle(
mesh_handle : @render3d_types.MeshHandle,
) -> Unit {
let prefix = triangle_mesh_cache_prefix(mesh_handle)
for pair in backend.triangle_mesh_cache.to_array() {
if pair.0.has_prefix(prefix) {
@raylib.unload_mesh(pair.1)
backend.triangle_mesh_cache.remove(pair.0)
}
}
}
///|
fn to_fixed_vec3_array(values : Array[@smath.Vec3]) -> FixedArray[Float] {
let out = FixedArray::make(values.length() * 3, to_float(0.0))
for index in 0.. FixedArray[Float] {
let out = FixedArray::make(values.length() * 2, to_float(0.0))
for index in 0.. FixedArray[Byte] {
let out = FixedArray::make(values.length() * 4, b'\x00')
for index in 0.. FixedArray[Float] {
let out = FixedArray::make(values.length() * 4, to_float(0.0))
for index in 0.. Array[Int] {
let indices : Array[Int] = []
for index in 0.. Array[Int] {
let triangle_indices : Array[Int] = []
let push_index = fn(index : Int) { triangle_indices.push(index) }
match topology {
TriangleList => {
let tri_count = indices.length() / 3
for tri in 0..
if indices.length() >= 3 {
for i in 0..<(indices.length() - 2) {
if i % 2 == 0 {
push_index(indices[i])
push_index(indices[i + 1])
push_index(indices[i + 2])
} else {
push_index(indices[i + 1])
push_index(indices[i])
push_index(indices[i + 2])
}
}
}
TriangleFan =>
if indices.length() >= 3 {
let head = indices[0]
for i in 1..<(indices.length() - 1) {
push_index(head)
push_index(indices[i])
push_index(indices[i + 1])
}
}
}
triangle_indices
}
///|
fn resolve_triangle_indices(
mesh_handle : @render3d_types.MeshHandle,
mesh : @render3d_types.TriangleMesh3D,
) -> Array[Int]? {
let base_indices = mesh.indices.unwrap_or(
default_mesh_indices(mesh.positions.length()),
)
if base_indices.length() == 0 {
return None
}
for index in base_indices {
if index < 0 || index >= mesh.positions.length() {
warn_texture_issue(
"triangles_invalid_index:\{mesh_handle}:\{index}",
"triangle mesh \{mesh_handle} contains out-of-range index \{index}; skipping draw",
)
return None
}
}
let triangle_indices = triangulate_triangle_indices(
base_indices,
mesh.topology,
)
if triangle_indices.length() < 3 || triangle_indices.length() % 3 != 0 {
return None
}
Some(triangle_indices)
}
///|
fn sanitize_triangle_uv_set(
mesh_handle : @render3d_types.MeshHandle,
label : String,
set : Int,
) -> Int {
if set == 0 || set == 1 {
set
} else {
warn_texture_issue(
"triangles_unsupported_uv_set:\{mesh_handle}:\{label}:\{set}",
"triangle mesh \{mesh_handle} uses unsupported \{label} UV set TEXCOORD_\{set}; fallback to TEXCOORD_0",
)
0
}
}
///|
fn resolve_triangle_uv_sets(
mesh : @render3d_types.TriangleMesh3D,
mesh_handle : @render3d_types.MeshHandle,
texture_bundle : MaterialTextureBundle3D?,
) -> (MaterialTextureBundle3D?, Array[@smath.Vec2D]?, Array[@smath.Vec2D]?) {
let vertex_count = mesh.positions.length()
match texture_bundle {
Some(bundle) => {
let base_set = sanitize_triangle_uv_set(
mesh_handle,
"base_color_texture",
bundle.base_texcoord_set,
)
let emissive_set = sanitize_triangle_uv_set(
mesh_handle,
"emissive_texture",
bundle.emissive_texcoord_set,
)
let metallic_set = sanitize_triangle_uv_set(
mesh_handle,
"metallic_roughness_texture",
bundle.metallic_roughness_texcoord_set,
)
let occlusion_set = sanitize_triangle_uv_set(
mesh_handle,
"occlusion_texture",
bundle.occlusion_texcoord_set,
)
let normal_set = sanitize_triangle_uv_set(
mesh_handle,
"normal_texture",
bundle.normal_texcoord_set,
)
let uv0 = mesh.uv_sets
.get(0)
.filter(fn(values) { values.length() == vertex_count })
let uv1 = mesh.uv_sets
.get(1)
.filter(fn(values) { values.length() == vertex_count })
let has_uv0 = uv0 is Some(_)
let has_uv1 = uv1 is Some(_)
let mut has_base = bundle.has_base_texture
let mut has_emissive = bundle.has_emissive_texture
let mut has_metallic = bundle.has_metallic_roughness_texture
let mut has_occlusion = bundle.has_occlusion_texture
let mut has_normal = bundle.has_normal_texture
if (has_base && base_set == 0 && !has_uv0) ||
(has_emissive && emissive_set == 0 && !has_uv0) ||
(has_metallic && metallic_set == 0 && !has_uv0) ||
(has_occlusion && occlusion_set == 0 && !has_uv0) ||
(has_normal && normal_set == 0 && !has_uv0) {
warn_texture_issue(
"triangles_missing_uv_set0:\{mesh_handle}",
"triangle mesh \{mesh_handle} is missing TEXCOORD_0; texture maps that require UV0 are disabled",
)
if base_set == 0 {
has_base = false
}
if emissive_set == 0 {
has_emissive = false
}
if metallic_set == 0 {
has_metallic = false
}
if occlusion_set == 0 {
has_occlusion = false
}
if normal_set == 0 {
has_normal = false
}
}
if (has_base && base_set == 1 && !has_uv1) ||
(has_emissive && emissive_set == 1 && !has_uv1) ||
(has_metallic && metallic_set == 1 && !has_uv1) ||
(has_occlusion && occlusion_set == 1 && !has_uv1) ||
(has_normal && normal_set == 1 && !has_uv1) {
warn_texture_issue(
"triangles_missing_uv_set1:\{mesh_handle}",
"triangle mesh \{mesh_handle} is missing TEXCOORD_1; texture maps that require UV1 are disabled",
)
if base_set == 1 {
has_base = false
}
if emissive_set == 1 {
has_emissive = false
}
if metallic_set == 1 {
has_metallic = false
}
if occlusion_set == 1 {
has_occlusion = false
}
if normal_set == 1 {
has_normal = false
}
}
if !has_base &&
!has_emissive &&
!has_metallic &&
!has_occlusion &&
!has_normal {
(None, uv0, uv1)
} else {
(
Some({
..bundle,
has_base_texture: has_base,
has_emissive_texture: has_emissive,
has_metallic_roughness_texture: has_metallic,
has_occlusion_texture: has_occlusion,
has_normal_texture: has_normal,
base_texcoord_set: base_set,
emissive_texcoord_set: emissive_set,
metallic_roughness_texcoord_set: metallic_set,
occlusion_texcoord_set: occlusion_set,
normal_texcoord_set: normal_set,
}),
uv0,
uv1,
)
}
}
None => (None, None, None)
}
}
///|
fn expand_vec3_with_indices(
values : Array[@smath.Vec3],
indices : Array[Int],
) -> Array[@smath.Vec3]? {
let output : Array[@smath.Vec3] = []
for index in indices {
guard values.get(index) is Some(value) else { return None }
output.push(value)
}
Some(output)
}
///|
fn expand_vec2_with_indices(
values : Array[@smath.Vec2D],
indices : Array[Int],
) -> Array[@smath.Vec2D]? {
let output : Array[@smath.Vec2D] = []
for index in indices {
guard values.get(index) is Some(value) else { return None }
output.push(value)
}
Some(output)
}
///|
fn expand_color_with_indices(
values : Array[@render.Color],
indices : Array[Int],
) -> Array[@render.Color]? {
let output : Array[@render.Color] = []
for index in indices {
guard values.get(index) is Some(value) else { return None }
output.push(value)
}
Some(output)
}
///|
fn expand_tangent_with_indices(
values : Array[@render3d_types.Tangent3D],
indices : Array[Int],
) -> Array[@render3d_types.Tangent3D]? {
let output : Array[@render3d_types.Tangent3D] = []
for index in indices {
guard values.get(index) is Some(value) else { return None }
output.push(value)
}
Some(output)
}
///|
fn create_uploaded_triangle_mesh(
vertices : Array[@smath.Vec3],
uv0 : Array[@smath.Vec2D]?,
uv1 : Array[@smath.Vec2D]?,
normals : Array[@smath.Vec3],
tangents : Array[@render3d_types.Tangent3D]?,
colors : Array[@render.Color]?,
) -> @raylib.Mesh? {
let vertex_count = vertices.length()
if vertex_count < 3 || vertex_count % 3 != 0 {
return None
}
if normals.length() != vertex_count {
return None
}
let fixed_vertices = to_fixed_vec3_array(vertices)
let fixed_normals = to_fixed_vec3_array(normals)
let texcoords = uv0.map_or(
FixedArray::make(vertex_count * 2, to_float(0.0)),
to_fixed_vec2_array,
)
let texcoords2 = uv1.map_or(
FixedArray::make(vertex_count * 2, to_float(0.0)),
to_fixed_vec2_array,
)
let tangents = tangents.map_or(
{
let defaults : Array[@render3d_types.Tangent3D] = []
for _ in 0.. @raylib.Texture {
match slot {
0 => bundle.base_texture
1 => bundle.metallic_roughness_texture
2 => bundle.normal_texture
4 => bundle.occlusion_texture
5 => bundle.emissive_texture
_ => get_white_texture()
}
}
///|
fn null_texture() -> @raylib.Texture {
@raylib.Texture::new(0U, 0, 0, 0, 0)
}
///|
fn sync_shadow_sampler_uniforms(
state : LightingShaderState,
shadow_state : FrameShadowState?,
) -> Unit {
let shader = state.shader
for index in 0.. Unit {
let lighting_state = upload_lighting_uniforms(
frame, camera_position, material, texture_bundle, mesh_has_tangents, receive_shadows,
shadow_state,
)
let effective_bundle = texture_bundle.unwrap_or(
fallback_lighting_texture_bundle(),
)
let ray_material = get_lit_material(texture_bundle is Some(_))
for binding in material_texture_slot_bindings() {
@raylib.set_material_texture(
ray_material,
binding.material_map,
material_texture_for_slot(effective_bundle, binding.slot),
)
}
@raylib.set_material_texture(
ray_material,
@raylib.MaterialMapRoughness,
null_texture(),
)
if material.double_sided {
@rl.disable_backface_culling()
} else {
@rl.enable_backface_culling()
}
bind_shadow_textures(shadow_state)
sync_shadow_sampler_uniforms(lighting_state, shadow_state)
@raylib.draw_mesh(mesh, ray_material, @raylib.Matrix::identity())
unbind_shadow_textures(shadow_state)
@rl.disable_backface_culling()
}
///|
fn get_texture(path : String) -> @raylib.Texture {
if backend.textures.get(path) is Some(texture) {
return texture
}
let texture = if get_embedded_asset(path) is Some(bytes) {
let image = @raylib.load_image_from_memory(
file_extension(path),
bytes,
bytes.length(),
)
let texture = @raylib.load_texture_from_image(image)
@raylib.unload_image(image)
texture
} else if path.to_lower().has_prefix("data:") {
match decode_data_uri_base64(path) {
Some(bytes) => {
let image = @raylib.load_image_from_memory(
file_extension(path),
bytes,
bytes.length(),
)
let texture = @raylib.load_texture_from_image(image)
@raylib.unload_image(image)
texture
}
None => @raylib.load_texture(path)
}
} else {
@raylib.load_texture(path)
}
@raylib.set_texture_filter(texture, configured_filter())
backend.textures.set(path, texture)
texture
}
///|
fn get_sound(path : String) -> @raylib.Sound {
if backend.sounds.get(path) is Some(sound) {
return sound
}
let sound = if get_embedded_asset(path) is Some(bytes) {
let wave = @raylib.load_wave_from_memory(
file_extension(path),
bytes,
bytes.length(),
)
let sound = @raylib.load_sound_from_wave(wave)
@raylib.unload_wave(wave)
sound
} else {
@raylib.load_sound(path)
}
backend.sounds.set(path, sound)
sound
}
///|
fn create_music(path : String) -> @raylib.Music {
if get_embedded_asset(path) is Some(bytes) {
@raylib.load_music_stream_from_memory(
file_extension(path),
bytes,
bytes.length(),
)
} else {
@raylib.load_music_stream(path)
}
}
///|
fn next_audio_instance() -> AudioInstance {
let instance = backend.next_audio_instance.reinterpret_as_int()
backend.next_audio_instance += 1U
instance
}
///|
fn free_audio_playback(playback : AudioPlayback) -> Unit {
match playback.kind {
Sound(sound) => {
@raylib.stop_sound(sound)
@raylib.unload_sound_alias(sound)
}
Music(music) => {
@raylib.stop_music_stream(music)
@raylib.unload_music_stream(music)
}
}
}
///|
fn tick_audio_playback(playback : AudioPlayback) -> Unit {
if playback.finished || playback.paused {
return
}
match playback.kind {
Sound(sound) =>
if playback.started && !@raylib.is_sound_playing(sound) {
if playback.loop_ {
@raylib.play_sound(sound)
} else {
playback.finished = true
}
}
Music(music) => {
@raylib.update_music_stream(music)
if playback.started && !@raylib.is_music_stream_playing(music) {
if playback.loop_ {
@raylib.play_music_stream(music)
} else {
playback.finished = true
}
}
}
}
}
///|
fn mark_frame_drawn() -> Unit {
backend.frame_has_draw_commands = true
}
///|
fn get_font_by_family(family : String) -> @raylib.Font {
if backend.font_alias.get(family) is Some(path) {
if backend.fonts_by_path.get(path) is Some(font) {
return font
}
}
@raylib.Font::default()
}
///|
fn reset_cosmic_runtime() -> Unit {
backend.cosmic_loaded_families.clear()
for _key, entry in backend.cosmic_textures {
@raylib.unload_texture(entry.texture)
}
backend.cosmic_textures.clear()
cosmic_font_system.val = @cosmic.FontSystem::new()
cosmic_swash_cache.val = @cosmic.SwashCache::new()
}
///|
fn ensure_cosmic_font_loaded(family : String) -> Bool {
if backend.cosmic_loaded_families.contains(family) {
return true
}
guard backend.font_alias.get(family) is Some(path) else { return false }
guard get_asset_bytes(path) is Some(bytes) else { return false }
cosmic_font_system.val = cosmic_font_system.val.load_font_data(bytes)
backend.cosmic_loaded_families.add(family)
true
}
///|
fn cosmic_text_cache_key(
text : String,
style : @render2d_types.TextStyle2D,
) -> String {
let color = style.color
let alpha = clamp_u8((color.a * 255.0).round().to_int())
"t:" +
text.length().to_string() +
":" +
text +
"|f:" +
style.family.length().to_string() +
":" +
style.family +
"|s:" +
style.size.to_string() +
"|r:" +
color.r.to_string() +
"|g:" +
color.g.to_string() +
"|b:" +
color.b.to_string() +
"|a:" +
alpha.to_string()
}
///|
fn cosmic_font_size(size : Double) -> Double {
if size <= 0.0 {
1.0
} else {
size
}
}
///|
fn rasterize_with_cosmic(
text : String,
style : @render2d_types.TextStyle2D,
) -> CosmicRasterImage? {
guard text != "" else { return None }
guard ensure_cosmic_font_loaded(style.family) else { return None }
let font_size = cosmic_font_size(style.size)
let metrics = @cosmic.Metrics::new(
Float::from_double(font_size),
Float::from_double(font_size * 1.25),
)
let attrs = @cosmic.Attrs::new().family(@cosmic.Family::Name(style.family))
let attrs_list = @cosmic.AttrsList::new(attrs)
let buffer = @cosmic.Buffer::new(metrics)
.set_wrap(@cosmic.Wrap::None)
.set_text(text, attrs_list, @cosmic.Shaping::Advanced)
.set_size(None, None)
let mut min_x = 0
let mut min_y = 0
let mut max_x = -1
let mut max_y = -1
let fills : Array[(Int, Int, Int, Int, Int, Int, Int, Int)] = []
let (drawn_buffer, swash_cache) = buffer.draw(
cosmic_font_system.val,
cosmic_swash_cache.val,
@cosmic.Color::rgba(
style.color.r.to_byte(),
style.color.g.to_byte(),
style.color.b.to_byte(),
clamp_u8((style.color.a * 255.0).round().to_int()).to_byte(),
),
fn(x, y, width, height, color) {
let w = width.reinterpret_as_int()
let h = height.reinterpret_as_int()
guard w > 0 && h > 0 else { return }
let (r, g, b, a) = color.as_rgba()
fills.push((x, y, w, h, r.to_int(), g.to_int(), b.to_int(), a.to_int()))
let right = x + w - 1
let bottom = y + h - 1
if max_x < min_x {
min_x = x
min_y = y
max_x = right
max_y = bottom
} else {
if x < min_x {
min_x = x
}
if y < min_y {
min_y = y
}
if right > max_x {
max_x = right
}
if bottom > max_y {
max_y = bottom
}
}
},
)
ignore(drawn_buffer)
cosmic_swash_cache.val = swash_cache
guard !fills.is_empty() else { return None }
let width = max_x - min_x + 1
let height = max_y - min_y + 1
let pixels = Array::make(width * height * 4, 0)
for fill in fills {
let x = fill.0
let y = fill.1
let w = fill.2
let h = fill.3
let r = fill.4
let g = fill.5
let b = fill.6
let a = fill.7
for local_y in 0..= height {
continue
}
for local_x in 0..= width {
continue
}
let idx = (py * width + px) * 4
pixels[idx] = r
pixels[idx + 1] = g
pixels[idx + 2] = b
pixels[idx + 3] = a
}
}
}
Some({ width, height, pixels })
}
///|
fn texture_from_cosmic_raster(raster : CosmicRasterImage) -> CosmicTextTexture? {
guard raster.width > 0 && raster.height > 0 else { return None }
let image = @raylib.gen_image_color(
raster.width,
raster.height,
@raylib.Color::new(0, 0, 0, 0),
)
let texture = @raylib.load_texture_from_image(image)
@raylib.unload_image(image)
@raylib.update_texture(
texture,
Bytes::from_iter(
raster.pixels.iter().map(fn(value) { clamp_u8(value).to_byte() }),
),
)
@raylib.set_texture_filter(texture, configured_filter())
Some({ texture, width: raster.width, height: raster.height })
}
///|
fn cosmic_texture_for_text(
text : String,
style : @render2d_types.TextStyle2D,
) -> CosmicTextTexture? {
let key = cosmic_text_cache_key(text, style)
if backend.cosmic_textures.get(key) is Some(cached) {
return Some(cached)
}
guard rasterize_with_cosmic(text, style) is Some(raster) else { return None }
guard texture_from_cosmic_raster(raster) is Some(created) else { return None }
backend.cosmic_textures.set(key, created)
Some(created)
}
///|
fn measure_text_with_cosmic(
style : @render2d_types.TextStyle2D,
text : String,
) -> @smath.Vec2D? {
guard ensure_cosmic_font_loaded(style.family) else { return None }
let font_size = cosmic_font_size(style.size)
let metrics = @cosmic.Metrics::new(
Float::from_double(font_size),
Float::from_double(font_size * 1.25),
)
let attrs = @cosmic.Attrs::new().family(@cosmic.Family::Name(style.family))
let attrs_list = @cosmic.AttrsList::new(attrs)
let buffer = @cosmic.Buffer::new(metrics)
.set_wrap(@cosmic.Wrap::None)
.set_text(text, attrs_list, @cosmic.Shaping::Advanced)
.set_size(None, None)
.layout_all_with_font_system(cosmic_font_system.val)
let mut width = 0.0
let mut height = 0.0
for run in buffer.layout_runs() {
width = @cmp.maximum(width, run.line_w.to_double())
height = @cmp.maximum(height, (run.line_top + run.line_height).to_double())
}
if height <= 0.0 {
height = font_size
}
Some(@smath.Vec2D(width, height))
}
///|
fn draw_text_with_cosmic(command : @render2d_types.TextDrawCommand2D) -> Bool {
guard command.text != "" else { return true }
guard command.style.color.a > 0.0 else { return true }
guard cosmic_texture_for_text(command.text, command.style)
is Some(cached_texture) else {
return false
}
let measured = measure_text_with_cosmic(command.style, command.text).unwrap_or(
@smath.Vec2D(
cached_texture.width.to_double(),
cached_texture.height.to_double(),
),
)
let mut x = command.position[X]
let mut y = command.position[Y]
match command.style.align {
Left => ()
Center => x -= measured[X] / 2.0
Right => x -= measured[X]
}
match command.style.baseline {
Top => ()
Center => y -= measured[Y] / 2.0
Bottom => y -= measured[Y]
}
@raylib.draw_texture_pro(
cached_texture.texture,
to_ray_rect(
0.0,
0.0,
cached_texture.width.to_double(),
cached_texture.height.to_double(),
),
to_ray_rect(
x,
y,
cached_texture.width.to_double(),
cached_texture.height.to_double(),
),
to_ray_vector2(0.0, 0.0),
0.0,
to_ray_color(white_render_color()),
)
true
}
///|
fn key_to_raylib(code : @inputs.Code) -> Int {
match code {
KeyA => @raylib.KeyA
KeyB => @raylib.KeyB
KeyC => @raylib.KeyC
KeyD => @raylib.KeyD
KeyE => @raylib.KeyE
KeyF => @raylib.KeyF
KeyG => @raylib.KeyG
KeyH => @raylib.KeyH
KeyI => @raylib.KeyI
KeyJ => @raylib.KeyJ
KeyK => @raylib.KeyK
KeyL => @raylib.KeyL
KeyM => @raylib.KeyM
KeyN => @raylib.KeyN
KeyO => @raylib.KeyO
KeyP => @raylib.KeyP
KeyQ => @raylib.KeyQ
KeyR => @raylib.KeyR
KeyS => @raylib.KeyS
KeyT => @raylib.KeyT
KeyU => @raylib.KeyU
KeyV => @raylib.KeyV
KeyW => @raylib.KeyW
KeyX => @raylib.KeyX
KeyY => @raylib.KeyY
KeyZ => @raylib.KeyZ
ArrowUp => @raylib.KeyUp
ArrowDown => @raylib.KeyDown
ArrowLeft => @raylib.KeyLeft
ArrowRight => @raylib.KeyRight
Tab => @raylib.KeyTab
ShiftLeft => @raylib.KeyLeftShift
Space => @raylib.KeySpace
Enter => @raylib.KeyEnter
Escape => @raylib.KeyEscape
}
}
///|
fn all_codes() -> Array[@inputs.Code] {
[
KeyA,
KeyB,
KeyC,
KeyD,
KeyE,
KeyF,
KeyG,
KeyH,
KeyI,
KeyJ,
KeyK,
KeyL,
KeyM,
KeyN,
KeyO,
KeyP,
KeyQ,
KeyR,
KeyS,
KeyT,
KeyU,
KeyV,
KeyW,
KeyX,
KeyY,
KeyZ,
ArrowUp,
ArrowDown,
ArrowLeft,
ArrowRight,
Tab,
ShiftLeft,
Space,
Enter,
Escape,
]
}
///|
fn sync_key_events() -> Unit {
if backend.key_events is Some(pressed_keys) {
for code in all_codes() {
if @raylib.is_key_down(key_to_raylib(code)) {
pressed_keys.add(code)
} else {
pressed_keys.remove(code)
}
}
}
}
///|
fn sync_mouse_events() -> Unit {
if backend.mouse_state is Some(mouse) &&
backend.mouse_movement is Some(movement) {
let pos = @raylib.get_mouse_position() |> to_selene_vec2
let delta = @raylib.get_mouse_delta() |> to_selene_vec2
let wheel = @raylib.get_mouse_wheel_move_v() |> to_selene_vec2
// Keep raw screen-space mouse coordinates consistent with canvas backend.
// Core click systems are responsible for applying zoom conversion.
mouse.pos = @smath.Vec2D(pos[X], pos[Y])
movement.movement = @smath.Vec2D(delta[X], delta[Y])
if backend.mouse_wheel is Some(mouse_wheel) {
mouse_wheel.delta = @smath.Vec2D(wheel[X], wheel[Y])
}
mouse.left_button = @raylib.is_mouse_button_down(@raylib.MouseButtonLeft)
mouse.right_button = @raylib.is_mouse_button_down(@raylib.MouseButtonRight)
mouse.middle_button = @raylib.is_mouse_button_down(
@raylib.MouseButtonMiddle,
)
}
}
///|
const MAX_GAMEPADS : Int = 8
///|
fn sync_gamepad_events() -> Unit {
guard backend.gamepads_state is Some(gamepads) else { return }
guard backend.gamepad_buttons_state is Some(buttons) else { return }
guard backend.gamepad_axes_state is Some(axes) else { return }
gamepads.clear()
buttons.clear()
axes.clear()
for id in 0.. Unit {
let mappings : Array[(Int, @inputs.GamepadButton)] = [
(@raylib.GamepadButtonRightFaceDown, South),
(@raylib.GamepadButtonRightFaceRight, East),
(@raylib.GamepadButtonRightFaceUp, North),
(@raylib.GamepadButtonRightFaceLeft, West),
(@raylib.GamepadButtonLeftTrigger1, LeftTrigger),
(@raylib.GamepadButtonLeftTrigger2, LeftTrigger2),
(@raylib.GamepadButtonRightTrigger1, RightTrigger),
(@raylib.GamepadButtonRightTrigger2, RightTrigger2),
(@raylib.GamepadButtonMiddleLeft, Select),
(@raylib.GamepadButtonMiddleRight, Start),
(@raylib.GamepadButtonMiddle, Mode),
(@raylib.GamepadButtonLeftThumb, LeftThumb),
(@raylib.GamepadButtonRightThumb, RightThumb),
(@raylib.GamepadButtonLeftFaceUp, DPadUp),
(@raylib.GamepadButtonLeftFaceDown, DPadDown),
(@raylib.GamepadButtonLeftFaceLeft, DPadLeft),
(@raylib.GamepadButtonLeftFaceRight, DPadRight),
]
for mapping in mappings {
if @raylib.is_gamepad_button_down(id, mapping.0) {
buttons.add(@inputs.GamepadButtonInput::new(gamepad, mapping.1))
}
}
}
///|
fn sync_one_gamepad_axes(
id : Int,
gamepad : @inputs.Gamepad,
axes : Map[@inputs.GamepadAxisInput, Double],
) -> Unit {
axes.set(
@inputs.GamepadAxisInput::new(gamepad, LeftStickX),
@raylib.get_gamepad_axis_movement(id, @raylib.GamepadAxisLeftX).to_double(),
)
axes.set(
@inputs.GamepadAxisInput::new(gamepad, LeftStickY),
@raylib.get_gamepad_axis_movement(id, @raylib.GamepadAxisLeftY).to_double(),
)
axes.set(
@inputs.GamepadAxisInput::new(gamepad, RightStickX),
@raylib.get_gamepad_axis_movement(id, @raylib.GamepadAxisRightX).to_double(),
)
axes.set(
@inputs.GamepadAxisInput::new(gamepad, RightStickY),
@raylib.get_gamepad_axis_movement(id, @raylib.GamepadAxisRightY).to_double(),
)
axes.set(
@inputs.GamepadAxisInput::new(gamepad, LeftZ),
@raylib.get_gamepad_axis_movement(id, @raylib.GamepadAxisLeftTrigger).to_double(),
)
axes.set(
@inputs.GamepadAxisInput::new(gamepad, RightZ),
@raylib.get_gamepad_axis_movement(id, @raylib.GamepadAxisRightTrigger).to_double(),
)
}
///|
fn sync_mouse_lock() -> Unit {
if backend.lock_state is Some(lock_state) {
// Keep requested lock state separate from runtime cursor visibility.
if lock_state.val != backend.cursor_locked {
backend.lock_requested = lock_state.val
}
if backend.lock_requested {
if !backend.cursor_locked && @raylib.is_window_focused() {
@raylib.disable_cursor()
}
} else if backend.cursor_locked {
@raylib.enable_cursor()
}
backend.cursor_locked = @raylib.is_cursor_hidden()
lock_state.val = backend.cursor_locked
}
}
///|
fn update_audio_instances() -> Unit {
for _instance, playback in backend.audio_instances {
tick_audio_playback(playback)
}
for pair in backend.audio_instances.to_array() {
if pair.1.finished {
free_audio_playback(pair.1)
backend.audio_instances.remove(pair.0)
}
}
}
///|
fn cleanup_resources() -> Unit {
for pair in backend.audio_instances.to_array() {
free_audio_playback(pair.1)
}
backend.audio_instances.clear()
for _path, sound in backend.sounds {
@raylib.unload_sound(sound)
}
backend.sounds.clear()
for _path, font in backend.fonts_by_path {
@raylib.unload_font(font)
}
backend.fonts_by_path.clear()
backend.font_alias.clear()
reset_cosmic_runtime()
backend.embedded_asset_cache.clear()
for _key, mesh in backend.primitive_meshes {
@raylib.unload_mesh(mesh)
}
backend.primitive_meshes.clear()
for _key, mesh in backend.triangle_mesh_cache {
@raylib.unload_mesh(mesh)
}
backend.triangle_mesh_cache.clear()
let cleanup_default_material = if backend.lit_material is Some(_) ||
backend.lit_textured_material is Some(_) ||
backend.shadow_material is Some(_) {
Some(@raylib.Material::default())
} else {
None
}
if backend.lit_material is Some(material) {
if cleanup_default_material is Some(default_material) {
reset_material_from_material(material, default_material)
}
@raylib.unload_material(material)
}
backend.lit_material = None
if backend.lit_textured_material is Some(material) {
if cleanup_default_material is Some(default_material) {
reset_material_from_material(material, default_material)
}
@raylib.unload_material(material)
}
backend.lit_textured_material = None
if backend.shadow_material is Some(material) {
if cleanup_default_material is Some(default_material) {
reset_material_from_material(material, default_material)
}
@raylib.unload_material(material)
}
backend.shadow_material = None
if cleanup_default_material is Some(material) {
@raylib.unload_material(material)
}
if backend.lighting_shader is Some(state) {
@raylib.unload_shader(state.shader)
}
backend.lighting_shader = None
if backend.shadow_shader is Some(state) {
@raylib.unload_shader(state.shader)
}
backend.shadow_shader = None
for _path, texture in backend.textures {
@raylib.unload_texture(texture)
}
backend.textures.clear()
for render_texture in backend.shadow_render_textures {
render_texture.unload()
}
backend.shadow_render_textures.clear()
backend.directional_shadow_tile_sizes.clear()
backend.directional_shadow_atlas_columns.clear()
backend.directional_shadow_atlas_rows.clear()
if backend.spot_shadow_render_texture is Some(render_texture) {
render_texture.unload()
}
backend.spot_shadow_render_texture = None
backend.spot_shadow_map_size = 0
if backend.point_shadow_render_texture is Some(render_texture) {
render_texture.unload()
}
backend.point_shadow_render_texture = None
backend.point_shadow_map_size = 0
backend.meshes3d.clear()
backend.materials3d.clear()
backend.images3d.clear()
backend.texture_warning_keys.clear()
}
///|
pub fn initialize(
config : @runtime.WindowConfig,
callbacks : @runtime.RunnerCallbacks,
) -> () -> Unit {
ignore(config.viewport_width)
ignore(config.viewport_height)
backend.image_smooth = config.image_smooth
backend.close_requested = false
fn() {
@raylib.init_window(
config.screen_width.to_int(),
config.screen_height.to_int(),
"Selene (raylib)",
)
@raylib.set_exit_key(@raylib.KeyNull)
@raylib.set_target_fps(config.fps.reinterpret_as_int())
if !@raylib.is_audio_device_ready() {
@raylib.init_audio_device()
}
backend.texture_warning_keys.clear()
(callbacks.startup)()
while !backend.close_requested && !@raylib.window_should_close() {
let delta = @raylib.get_frame_time().to_double()
backend.realtime_delta = delta
sync_key_events()
sync_mouse_events()
sync_gamepad_events()
sync_mouse_lock()
(callbacks.game_loop)(delta * backend.time_scale)
@raylib.begin_drawing()
@raylib.clear_background(@raylib.black)
backend.frame_has_draw_commands = false
(callbacks.render_loop)(delta * backend.time_scale)
@raylib.end_drawing()
}
cleanup_resources()
if @raylib.is_audio_device_ready() {
@raylib.close_audio_device()
}
@raylib.close_window()
}
}
///|
pub fn draw_image(
command : @render2d_types.ImageDrawCommand2D,
image_path : String,
) -> Unit {
mark_frame_drawn()
let texture = get_texture(image_path)
draw_image_repeat_transform(texture, command)
}
///|
pub fn push_clip_rect(rect : @smath.Rect) -> Unit {
let next = match clip_stack.last() {
Some(current) =>
clip_rect_intersection(current, rect).unwrap_or({
position: @smath.Vec2D::zero(),
size: @smath.Vec2D::zero(),
})
None => rect
}
clip_stack.push(next)
refresh_clip_stack()
}
///|
pub fn pop_clip_rect() -> Unit {
if clip_stack.is_empty() {
return
}
ignore(clip_stack.pop())
refresh_clip_stack()
}
///|
pub fn reset_clip_stack() -> Unit {
while !clip_stack.is_empty() {
ignore(clip_stack.pop())
}
@raylib.end_scissor_mode()
}
///|
pub fn register_key_events(pressed_keys : Set[@inputs.Code]) -> Unit {
backend.key_events = Some(pressed_keys)
}
///|
pub fn register_mouse_events(
mouse : @inputs.Mouse,
mouse_movement : @inputs.MouseMovement,
mouse_wheel : @inputs.MouseWheel,
) -> Unit {
backend.mouse_state = Some(mouse)
backend.mouse_movement = Some(mouse_movement)
backend.mouse_wheel = Some(mouse_wheel)
}
///|
pub fn register_gamepad_events(
gamepads : Set[@inputs.Gamepad],
buttons : Set[@inputs.GamepadButtonInput],
axes : Map[@inputs.GamepadAxisInput, Double],
) -> Unit {
backend.gamepads_state = Some(gamepads)
backend.gamepad_buttons_state = Some(buttons)
backend.gamepad_axes_state = Some(axes)
sync_gamepad_events()
}
///|
pub fn lock_mouse(locked : Ref[Bool]) -> Unit {
backend.lock_state = Some(locked)
backend.lock_requested = locked.val
sync_mouse_lock()
backend.cursor_locked = @raylib.is_cursor_hidden()
locked.val = backend.cursor_locked
}
///|
fn transform_is_identity_2d(transform : @smath.Transform) -> Bool {
(transform.a - 1.0).abs() < 0.000001 &&
transform.b.abs() < 0.000001 &&
transform.c.abs() < 0.000001 &&
(transform.d - 1.0).abs() < 0.000001 &&
transform.tx.abs() < 0.000001 &&
transform.ty.abs() < 0.000001
}
///|
fn draw_colored_quad(
p0 : (Float, Float),
p1 : (Float, Float),
p2 : (Float, Float),
p3 : (Float, Float),
c0 : @render.Color,
c1 : @render.Color,
c2 : @render.Color,
c3 : @render.Color,
) -> Unit {
ignore(@rl.check_render_batch_limit(4))
@rl.set_texture(0U)
@rl.begin(@rl.Quads)
@rl.normal3f(0.0, 0.0, 1.0)
let (r0, g0, b0, a0) = to_ray_color_bytes(c0)
@rl.color4ub(r0, g0, b0, a0)
@rl.vertex2f(p0.0, p0.1)
let (r1, g1, b1, a1) = to_ray_color_bytes(c1)
@rl.color4ub(r1, g1, b1, a1)
@rl.vertex2f(p1.0, p1.1)
let (r2, g2, b2, a2) = to_ray_color_bytes(c2)
@rl.color4ub(r2, g2, b2, a2)
@rl.vertex2f(p2.0, p2.1)
let (r3, g3, b3, a3) = to_ray_color_bytes(c3)
@rl.color4ub(r3, g3, b3, a3)
@rl.vertex2f(p3.0, p3.1)
@rl.end_()
}
///|
fn draw_line_loop(
points : Array[(Float, Float)],
color : @render.Color,
) -> Unit {
if points.length() < 2 {
return
}
let (r, g, b, a) = to_ray_color_bytes(color)
ignore(@rl.check_render_batch_limit(points.length() * 2))
@rl.set_texture(0U)
@rl.begin(@rl.Lines)
@rl.color4ub(r, g, b, a)
for index in 0.. Bool {
guard command.text != "" else { return true }
guard command.style.color.a > 0.0 else { return true }
guard cosmic_texture_for_text(command.text, command.style)
is Some(cached_texture) else {
return false
}
let measured = measure_text_with_cosmic(command.style, command.text).unwrap_or(
@smath.Vec2D(
cached_texture.width.to_double(),
cached_texture.height.to_double(),
),
)
let mut x = command.position[X]
let mut y = command.position[Y]
match command.style.align {
Left => ()
Center => x -= measured[X] / 2.0
Right => x -= measured[X]
}
match command.style.baseline {
Top => ()
Center => y -= measured[Y] / 2.0
Bottom => y -= measured[Y]
}
draw_textured_quad(
cached_texture.texture,
cached_texture.width.to_double(),
cached_texture.height.to_double(),
0.0,
0.0,
cached_texture.width.to_double(),
cached_texture.height.to_double(),
0.0,
0.0,
cached_texture.width.to_double(),
cached_texture.height.to_double(),
x,
y,
command.transform,
white_render_color(),
)
true
}
///|
pub fn draw_text(command : @render2d_types.TextDrawCommand2D) -> Unit {
mark_frame_drawn()
if !transform_is_identity_2d(command.transform) {
if draw_text_with_cosmic_transform(command) {
return
}
}
if draw_text_with_cosmic(command) {
return
}
let font = get_font_by_family(command.style.family)
let measured = measure_text(command.style, command.text)
let text_width = measured[X]
let text_height = measured[Y]
let mut x = command.position[X]
let mut y = command.position[Y]
match command.style.align {
Left => ()
Center => x -= text_width / 2.0
Right => x -= text_width
}
match command.style.baseline {
Top => ()
Center => y -= text_height / 2.0
Bottom => y -= text_height
}
@raylib.draw_text_ex(
font,
command.text,
to_ray_vector2(x, y),
to_float(command.style.size),
0.0,
to_ray_color(command.style.color),
)
}
///|
pub fn measure_text(
style : @render2d_types.TextStyle2D,
text : String,
) -> @smath.Vec2D {
if text == "" {
return @smath.Vec2D(0.0, cosmic_font_size(style.size))
}
if measure_text_with_cosmic(style, text) is Some(measured) {
return measured
}
let font = get_font_by_family(style.family)
let measured = @raylib.measure_text_ex(font, text, to_float(style.size), 0.0)
@smath.Vec2D(vector2_x(measured), vector2_y(measured))
}
///|
pub fn draw_rect(command : @render2d_types.RectDrawCommand2D) -> Unit {
mark_frame_drawn()
if !transform_is_identity_2d(command.transform) {
let p0 = transform_image_point(
command.rect.position[X],
command.rect.position[Y],
command.transform,
0.0,
0.0,
)
let p1 = transform_image_point(
command.rect.position[X],
command.rect.position[Y],
command.transform,
command.rect.size[X],
0.0,
)
let p2 = transform_image_point(
command.rect.position[X],
command.rect.position[Y],
command.transform,
command.rect.size[X],
command.rect.size[Y],
)
let p3 = transform_image_point(
command.rect.position[X],
command.rect.position[Y],
command.transform,
0.0,
command.rect.size[Y],
)
draw_colored_quad(
p0,
p1,
p2,
p3,
command.fill_color,
command.fill_color,
command.fill_color,
command.fill_color,
)
if command.stroke_color is Some(stroke_color) {
draw_line_loop([p0, p1, p2, p3], stroke_color)
}
return
}
let rect = to_ray_rect(
command.rect.position[X],
command.rect.position[Y],
command.rect.size[X],
command.rect.size[Y],
)
@raylib.draw_rectangle_rec(rect, to_ray_color(command.fill_color))
if command.stroke_color is Some(stroke_color) {
@raylib.draw_rectangle_lines_ex(rect, 1.0, to_ray_color(stroke_color))
}
}
///|
pub fn draw_circle(command : @render2d_types.CircleDrawCommand2D) -> Unit {
mark_frame_drawn()
if !transform_is_identity_2d(command.transform) {
let center = transform_image_point(
command.center[X],
command.center[Y],
command.transform,
0.0,
0.0,
)
let fill = command.fill_color
let outline : Array[(Float, Float)] = []
let segments = 40
ignore(@rl.check_render_batch_limit(segments * 3))
@rl.set_texture(0U)
@rl.begin(@rl.Triangles)
let (fr, fg, fb, fa) = to_ray_color_bytes(fill)
@rl.color4ub(fr, fg, fb, fa)
@rl.normal3f(0.0, 0.0, 1.0)
for index in 0.. Unit {
mark_frame_drawn()
if !transform_is_identity_2d(command.transform) {
let p0 = transform_image_point(
command.rect.position[X],
command.rect.position[Y],
command.transform,
0.0,
0.0,
)
let p1 = transform_image_point(
command.rect.position[X],
command.rect.position[Y],
command.transform,
command.rect.size[X],
0.0,
)
let p2 = transform_image_point(
command.rect.position[X],
command.rect.position[Y],
command.transform,
command.rect.size[X],
command.rect.size[Y],
)
let p3 = transform_image_point(
command.rect.position[X],
command.rect.position[Y],
command.transform,
0.0,
command.rect.size[Y],
)
draw_colored_quad(
p0,
p1,
p2,
p3,
command.color_start,
command.color_start,
command.color_end,
command.color_end,
)
return
}
let rect = to_ray_rect(
command.rect.position[X],
command.rect.position[Y],
command.rect.size[X],
command.rect.size[Y],
)
@raylib.draw_rectangle_gradient_ex(
rect,
to_ray_color(command.color_start),
to_ray_color(command.color_start),
to_ray_color(command.color_end),
to_ray_color(command.color_end),
)
}
///|
fn begin_3d(camera : @render3d_types.FrameCamera3D) -> Unit {
@raylib.begin_mode_3d(to_ray_camera3d(camera))
}
///|
fn end_3d() -> Unit {
@raylib.end_mode_3d()
}
///|
pub fn sync_mesh_asset(
handle : @render3d_types.MeshHandle,
mesh : @render3d_types.MeshAsset,
) -> Unit {
if backend.meshes3d.get(handle) is Some(existing) && existing != mesh {
clear_triangle_mesh_cache_for_handle(handle)
}
backend.meshes3d.set(handle, mesh)
}
///|
pub fn sync_material_asset(
handle : @render3d_types.MaterialHandle,
material : @render3d_types.StandardMaterial3D,
) -> Unit {
backend.materials3d.set(handle, material)
ensure_material_mipmaps(material)
}
///|
pub fn sync_image_asset(
handle : @render3d_types.ImageHandle,
image : @render3d_types.ImageAsset,
) -> Unit {
backend.images3d.set(handle, image)
if image_handle_requires_mipmaps(handle) {
ensure_texture_mipmaps(get_texture(image.path))
}
}
///|
pub fn release_mesh_asset(handle : @render3d_types.MeshHandle) -> Unit {
clear_triangle_mesh_cache_for_handle(handle)
backend.meshes3d.remove(handle)
}
///|
pub fn release_material_asset(handle : @render3d_types.MaterialHandle) -> Unit {
backend.materials3d.remove(handle)
}
///|
pub fn release_image_asset(handle : @render3d_types.ImageHandle) -> Unit {
backend.images3d.remove(handle)
}
///|
pub fn render_3d_frame(frame : @render3d_types.RenderFrame3D) -> Unit {
guard frame.camera is Some(active_camera) else { return }
let shadow_state = render_directional_shadow_map(frame, active_camera)
if !backend.frame_has_draw_commands {
@raylib.clear_background(to_ray_color(frame.clear_color))
}
mark_frame_drawn()
begin_3d(active_camera)
for item in frame.items {
guard backend.meshes3d.get(item.mesh) is Some(mesh_asset) else { continue }
let material = backend.materials3d
.get(item.material)
.unwrap_or(@render3d_types.default_standard_material3d())
draw_mesh3d_instance(
item.mesh,
mesh_asset,
material,
item.transform,
frame,
active_camera.position,
item.receive_shadows,
shadow_state,
)
}
end_3d()
}
///|
fn draw_mesh3d_instance(
mesh_handle : @render3d_types.MeshHandle,
mesh_asset : @render3d_types.MeshAsset,
material : @render3d_types.StandardMaterial3D,
transform : @render3d_types.FrameTransform3D,
frame : @render3d_types.RenderFrame3D,
camera_position : @smath.Vec3,
receive_shadows : Bool,
shadow_state : FrameShadowState?,
) -> Unit {
@rl.push_matrix()
@rl.translatef(
to_float(transform.translation.x),
to_float(transform.translation.y),
to_float(transform.translation.z),
)
apply_quat_rotation(transform.rotation)
@rl.scalef(
to_float(transform.scale.x),
to_float(transform.scale.y),
to_float(transform.scale.z),
)
let texture_bundle = resolve_material_texture_bundle(material)
match mesh_asset.primitive {
Cube(size) =>
draw_lit_cuboid(
size, material, frame, camera_position, texture_bundle, receive_shadows,
shadow_state,
)
Sphere(radius) =>
draw_lit_sphere(
radius, material, frame, camera_position, texture_bundle, receive_shadows,
shadow_state,
)
Cylinder(radius_top, radius_bottom, height, slices) =>
draw_lit_cylinder(
radius_top, radius_bottom, height, slices, material, frame, camera_position,
texture_bundle, receive_shadows, shadow_state,
)
Plane(size) => {
let thickness = if size.y.abs() < 0.0001 { 0.02 } else { size.y }
let cuboid = @smath.Vec3::new(size.x, thickness, size.z)
draw_lit_cuboid(
cuboid, material, frame, camera_position, texture_bundle, receive_shadows,
shadow_state,
)
}
Triangles(mesh) =>
draw_triangle_mesh(
mesh_handle, mesh, material, frame, camera_position, texture_bundle, receive_shadows,
shadow_state,
)
}
@rl.pop_matrix()
}
///|
const TEXTURED_SPHERE_STACKS : Int = 16
///|
const TEXTURED_SPHERE_SLICES : Int = 24
///|
fn draw_lit_cuboid(
size : @smath.Vec3,
material : @render3d_types.StandardMaterial3D,
frame : @render3d_types.RenderFrame3D,
camera_position : @smath.Vec3,
texture_bundle : MaterialTextureBundle3D?,
receive_shadows : Bool,
shadow_state : FrameShadowState?,
) -> Unit {
let key = "cube:\{size.x}:\{size.y}:\{size.z}"
let mesh = get_or_create_primitive_mesh(key, fn() {
@raylib.gen_mesh_cube(to_float(size.x), to_float(size.y), to_float(size.z))
})
draw_lit_mesh_with_material(
mesh, frame, camera_position, material, texture_bundle, false, receive_shadows,
shadow_state,
)
}
///|
fn draw_lit_sphere(
radius : Double,
material : @render3d_types.StandardMaterial3D,
frame : @render3d_types.RenderFrame3D,
camera_position : @smath.Vec3,
texture_bundle : MaterialTextureBundle3D?,
receive_shadows : Bool,
shadow_state : FrameShadowState?,
) -> Unit {
let key = "sphere:\{radius}:\{TEXTURED_SPHERE_SLICES}:\{TEXTURED_SPHERE_STACKS}"
let mesh = get_or_create_primitive_mesh(key, fn() {
@raylib.gen_mesh_sphere(
to_float(radius),
TEXTURED_SPHERE_SLICES,
TEXTURED_SPHERE_STACKS,
)
})
draw_lit_mesh_with_material(
mesh, frame, camera_position, material, texture_bundle, false, receive_shadows,
shadow_state,
)
}
///|
fn draw_lit_cylinder(
radius_top : Double,
radius_bottom : Double,
height : Double,
slices : Int,
material : @render3d_types.StandardMaterial3D,
frame : @render3d_types.RenderFrame3D,
camera_position : @smath.Vec3,
texture_bundle : MaterialTextureBundle3D?,
receive_shadows : Bool,
shadow_state : FrameShadowState?,
) -> Unit {
let safe_slices = @cmp.maximum(3, slices)
if (radius_top - radius_bottom).abs() <= 0.000001 {
let key = "cylinder:\{radius_top}:\{height}:\{safe_slices}"
let mesh = get_or_create_primitive_mesh(key, fn() {
@raylib.gen_mesh_cylinder(
to_float(radius_top),
to_float(height),
safe_slices,
)
})
draw_lit_mesh_with_material(
mesh, frame, camera_position, material, texture_bundle, false, receive_shadows,
shadow_state,
)
return
}
let key = "truncated_cylinder:\{radius_top}:\{radius_bottom}:\{height}:\{safe_slices}"
let mesh = get_or_create_primitive_mesh(key, fn() {
let (vertices, uvs, normals) = build_cylinder_mesh(
radius_top, radius_bottom, height, safe_slices,
)
create_uploaded_triangle_mesh(
vertices,
Some(uvs),
None,
normals,
None,
None,
).unwrap_or(
@raylib.gen_mesh_cylinder(
to_float(radius_bottom),
to_float(height),
safe_slices,
),
)
})
draw_lit_mesh_with_material(
mesh, frame, camera_position, material, texture_bundle, false, receive_shadows,
shadow_state,
)
}
///|
fn resolve_material_texture_source(
binding : @render3d_types.TextureBinding3D,
) -> MaterialTextureSource3D? {
guard backend.images3d.get(binding.image) is Some(image_asset) else {
warn_texture_issue(
"missing_image_handle:\{binding.image}",
"material texture handle \{binding.image} is not synced; fallback to default texture",
)
return None
}
let texture = get_texture(image_asset.path)
apply_texture_sampler(texture, binding.sampler)
let texture_id = @raylib.get_texture_id(texture)
let width = @raylib.get_texture_width(texture)
let height = @raylib.get_texture_height(texture)
if texture_id <= 0 || width <= 0 || height <= 0 {
warn_texture_issue(
"invalid_texture:\{image_asset.path}",
"failed to load texture '\{image_asset.path}' (id=\{texture_id}, size=\{width}x\{height}); fallback to base_color",
)
return None
}
Some({
texture,
texcoord_set: binding.texcoord_set,
transform: binding.transform,
})
}
///|
fn resolve_material_texture_bundle(
material : @render3d_types.StandardMaterial3D,
) -> MaterialTextureBundle3D? {
let base_source = material.base_color_texture.bind(
resolve_material_texture_source,
)
let emissive_source = material.emissive_texture.bind(
resolve_material_texture_source,
)
let metallic_roughness_source = material.metallic_roughness_texture.bind(
resolve_material_texture_source,
)
let occlusion_source = material.occlusion_texture.bind(
resolve_material_texture_source,
)
let normal_source = material.normal_texture.bind(
resolve_material_texture_source,
)
if base_source is None &&
emissive_source is None &&
metallic_roughness_source is None &&
occlusion_source is None &&
normal_source is None {
return None
}
let default_transform = @render3d_types.default_texture_transform3d()
Some({
base_texture: base_source.map_or(get_white_texture(), fn(source) {
source.texture
}),
emissive_texture: emissive_source.map_or(get_white_texture(), fn(source) {
source.texture
}),
metallic_roughness_texture: metallic_roughness_source.map_or(
get_white_texture(),
fn(source) { source.texture },
),
occlusion_texture: occlusion_source.map_or(get_white_texture(), fn(source) {
source.texture
}),
normal_texture: normal_source.map_or(null_texture(), fn(source) {
source.texture
}),
has_base_texture: base_source is Some(_),
has_emissive_texture: emissive_source is Some(_),
has_metallic_roughness_texture: metallic_roughness_source is Some(_),
has_occlusion_texture: occlusion_source is Some(_),
has_normal_texture: normal_source is Some(_),
base_transform: base_source.map_or(default_transform, fn(source) {
source.transform
}),
emissive_transform: emissive_source.map_or(default_transform, fn(source) {
source.transform
}),
metallic_roughness_transform: metallic_roughness_source.map_or(
default_transform,
fn(source) { source.transform },
),
occlusion_transform: occlusion_source.map_or(default_transform, fn(source) {
source.transform
}),
normal_transform: normal_source.map_or(default_transform, fn(source) {
source.transform
}),
base_texcoord_set: base_source.map_or(0, fn(source) { source.texcoord_set }),
emissive_texcoord_set: emissive_source.map_or(0, fn(source) {
source.texcoord_set
}),
metallic_roughness_texcoord_set: metallic_roughness_source.map_or(0, fn(
source,
) {
source.texcoord_set
}),
occlusion_texcoord_set: occlusion_source.map_or(0, fn(source) {
source.texcoord_set
}),
normal_texcoord_set: normal_source.map_or(0, fn(source) {
source.texcoord_set
}),
})
}
///|
fn texture_wrap_to_raylib(wrap : @render3d_types.TextureWrap3D) -> Int {
match wrap {
ClampToEdge => @raylib.TextureWrapClamp
MirroredRepeat => @raylib.TextureWrapMirrorRepeat
Repeat => @raylib.TextureWrapRepeat
}
}
///|
fn texture_filter_to_raylib(sampler : @render3d_types.TextureSampler3D) -> Int {
let nearest = sampler.mag_filter == Nearest || sampler.min_filter == Nearest
match sampler.mipmap_filter {
Some(_) =>
if nearest {
@raylib.TextureFilterPoint
} else {
@raylib.TextureFilterTrilinear
}
None =>
if nearest {
@raylib.TextureFilterPoint
} else {
@raylib.TextureFilterBilinear
}
}
}
///|
fn sampler_requests_generated_mipmaps(
sampler : @render3d_types.TextureSampler3D,
) -> Bool {
sampler.mipmap_filter is Some(_) &&
texture_filter_to_raylib(sampler) == @raylib.TextureFilterTrilinear
}
///|
fn maybe_ensure_binding_mipmaps(
binding : @render3d_types.TextureBinding3D,
) -> Unit {
if !sampler_requests_generated_mipmaps(binding.sampler) {
return
}
if backend.images3d.get(binding.image) is Some(image_asset) {
ensure_texture_mipmaps(get_texture(image_asset.path))
}
}
///|
fn ensure_material_mipmaps(
material : @render3d_types.StandardMaterial3D,
) -> Unit {
if material.base_color_texture is Some(binding) {
maybe_ensure_binding_mipmaps(binding)
}
if material.normal_texture is Some(binding) {
maybe_ensure_binding_mipmaps(binding)
}
if material.emissive_texture is Some(binding) {
maybe_ensure_binding_mipmaps(binding)
}
if material.metallic_roughness_texture is Some(binding) {
maybe_ensure_binding_mipmaps(binding)
}
if material.occlusion_texture is Some(binding) {
maybe_ensure_binding_mipmaps(binding)
}
}
///|
fn material_uses_image_with_mipmaps(
material : @render3d_types.StandardMaterial3D,
image : @render3d_types.ImageHandle,
) -> Bool {
let binding_matches = fn(binding : @render3d_types.TextureBinding3D) -> Bool {
binding.image == image &&
sampler_requests_generated_mipmaps(binding.sampler)
}
let uses_base = match material.base_color_texture {
Some(binding) => binding_matches(binding)
None => false
}
let uses_normal = match material.normal_texture {
Some(binding) => binding_matches(binding)
None => false
}
let uses_emissive = match material.emissive_texture {
Some(binding) => binding_matches(binding)
None => false
}
let uses_metallic_roughness = match material.metallic_roughness_texture {
Some(binding) => binding_matches(binding)
None => false
}
let uses_occlusion = match material.occlusion_texture {
Some(binding) => binding_matches(binding)
None => false
}
uses_base ||
uses_normal ||
uses_emissive ||
uses_metallic_roughness ||
uses_occlusion
}
///|
fn image_handle_requires_mipmaps(image : @render3d_types.ImageHandle) -> Bool {
for material_handle, material in backend.materials3d {
ignore(material_handle)
if material_uses_image_with_mipmaps(material, image) {
return true
}
}
false
}
///|
fn apply_texture_sampler(
texture : @raylib.Texture,
sampler : @render3d_types.TextureSampler3D,
) -> Unit {
@raylib.set_texture_wrap(texture, texture_wrap_to_raylib(sampler.wrap_u))
@raylib.set_texture_filter(texture, texture_filter_to_raylib(sampler))
}
///|
fn build_cylinder_mesh(
radius_top : Double,
radius_bottom : Double,
height : Double,
slices : Int,
) -> (Array[@smath.Vec3], Array[@smath.Vec2D], Array[@smath.Vec3]) {
let vertices : Array[@smath.Vec3] = []
let uvs : Array[@smath.Vec2D] = []
let normals : Array[@smath.Vec3] = []
let segment_count = if slices < 3 { 3 } else { slices }
let half_h = height / 2.0
let slope_y = if height.abs() < 0.000001 {
0.0
} else {
(radius_bottom - radius_top) / height
}
for segment in 0.. 0.000001 {
let top_center = @smath.Vec3::new(0.0, half_h, 0.0)
let top_normal = @smath.Vec3::new(0.0, 1.0, 0.0)
push_textured_triangle(
vertices,
uvs,
normals,
top_center,
top1,
top0,
@smath.Vec2D(0.5, 0.5),
@smath.Vec2D(0.5 + 0.5 * @cmath.cos(phi1), 0.5 + 0.5 * @cmath.sin(phi1)),
@smath.Vec2D(0.5 + 0.5 * @cmath.cos(phi0), 0.5 + 0.5 * @cmath.sin(phi0)),
top_normal,
top_normal,
top_normal,
)
}
if radius_bottom.abs() > 0.000001 {
let bottom_center = @smath.Vec3::new(0.0, -half_h, 0.0)
let bottom_normal = @smath.Vec3::new(0.0, -1.0, 0.0)
push_textured_triangle(
vertices,
uvs,
normals,
bottom_center,
bot0,
bot1,
@smath.Vec2D(0.5, 0.5),
@smath.Vec2D(0.5 + 0.5 * @cmath.cos(phi0), 0.5 + 0.5 * @cmath.sin(phi0)),
@smath.Vec2D(0.5 + 0.5 * @cmath.cos(phi1), 0.5 + 0.5 * @cmath.sin(phi1)),
bottom_normal,
bottom_normal,
bottom_normal,
)
}
}
(vertices, uvs, normals)
}
///|
fn push_textured_triangle(
vertices : Array[@smath.Vec3],
uvs : Array[@smath.Vec2D],
normals : Array[@smath.Vec3],
a : @smath.Vec3,
b : @smath.Vec3,
c : @smath.Vec3,
uv_a : @smath.Vec2D,
uv_b : @smath.Vec2D,
uv_c : @smath.Vec2D,
n_a : @smath.Vec3,
n_b : @smath.Vec3,
n_c : @smath.Vec3,
) -> Unit {
vertices.push(a)
vertices.push(b)
vertices.push(c)
uvs.push(uv_a)
uvs.push(uv_b)
uvs.push(uv_c)
normals.push(n_a)
normals.push(n_b)
normals.push(n_c)
}
///|
fn apply_quat_rotation(quat : @smath.Quat) -> Unit {
let normalized = quat.normalize()
let w = @cmp.minimum(1.0, @cmp.maximum(-1.0, normalized.w))
let angle = 2.0 * @cmath.acos(w)
if angle.abs() < 0.0000001 {
return
}
let sin_half = (1.0 - w * w).sqrt()
let axis = if sin_half < 0.000001 {
@smath.Vec3::new(0.0, 1.0, 0.0)
} else {
@smath.Vec3::new(
normalized.x / sin_half,
normalized.y / sin_half,
normalized.z / sin_half,
)
}
@rl.rotatef(
to_float(angle * 180.0 / @cmath.PI),
to_float(axis.x),
to_float(axis.y),
to_float(axis.z),
)
}
///|
fn draw_triangle_mesh(
mesh_handle : @render3d_types.MeshHandle,
mesh : @render3d_types.TriangleMesh3D,
material : @render3d_types.StandardMaterial3D,
frame : @render3d_types.RenderFrame3D,
camera_position : @smath.Vec3,
texture_bundle : MaterialTextureBundle3D?,
receive_shadows : Bool,
shadow_state : FrameShadowState?,
) -> Unit {
let vertices = mesh.positions
if vertices.length() < 3 {
warn_texture_issue(
"triangles_invalid_positions:\{mesh_handle}:\{vertices.length()}",
"triangle mesh positions length must be >= 3; skipping draw for \{mesh_handle}",
)
return
}
guard resolve_triangle_indices(mesh_handle, mesh) is Some(triangle_indices) else {
warn_texture_issue(
"triangles_invalid_indices:\{mesh_handle}",
"triangle mesh \{mesh_handle} has invalid index/topology data; skipping draw",
)
return
}
let (effective_bundle, uv0, uv1) = resolve_triangle_uv_sets(
mesh, mesh_handle, texture_bundle,
)
let mut normals = resolve_triangle_normals(mesh, triangle_indices)
if normals.length() != vertices.length() {
warn_texture_issue(
"triangles_normals_mismatch:\{normals.length()}:\{vertices.length()}",
"triangle mesh normal count (\{normals.length()}) != vertex count (\{vertices.length()}); fallback to default normals",
)
normals = []
for _ in 0.. Array[@smath.Vec3] {
mesh.normals
.filter(fn(candidates) { candidates.length() == mesh.positions.length() })
.unwrap_or(compute_vertex_normals(mesh.positions, triangle_indices))
}
///|
fn compute_vertex_normals(
positions : Array[@smath.Vec3],
triangle_indices : Array[Int],
) -> Array[@smath.Vec3] {
let normals : Array[@smath.Vec3] = []
for _ in 0..= positions.length() ||
ib < 0 ||
ib >= positions.length() ||
ic < 0 ||
ic >= positions.length() {
continue
}
let a = positions[ia]
let b = positions[ib]
let c = positions[ic]
let mut normal = (b - a).cross(c - a)
if normal.length_squared() <= 0.0000001 {
normal = @smath.Vec3::new(0.0, 1.0, 0.0)
}
normals[ia] = normals[ia] + normal
normals[ib] = normals[ib] + normal
normals[ic] = normals[ic] + normal
}
for index in 0.. AudioInstance {
let speed = clamp_playback_speed(speed)
let instance = next_audio_instance()
let playback = if loop_ {
let music = create_music(audio_path)
@raylib.set_music_volume(music, to_float(volume))
@raylib.set_music_pitch(music, to_float(speed))
if !paused {
@raylib.play_music_stream(music)
}
{
kind: Music(music),
loop_: true,
paused,
started: !paused,
finished: false,
}
} else {
let base_sound = get_sound(audio_path)
let sound = @raylib.load_sound_alias(base_sound)
@raylib.set_sound_volume(sound, to_float(volume))
@raylib.set_sound_pitch(sound, to_float(speed))
if !paused {
@raylib.play_sound(sound)
}
{
kind: Sound(sound),
loop_: false,
paused,
started: !paused,
finished: false,
}
}
backend.audio_instances.set(instance, playback)
instance
}
///|
pub fn set_volume(instance~ : AudioInstance, volume~ : Double) -> Unit {
guard backend.audio_instances.get(instance) is Some(playback) else { return }
match playback.kind {
Sound(sound) => @raylib.set_sound_volume(sound, to_float(volume))
Music(music) => @raylib.set_music_volume(music, to_float(volume))
}
}
///|
pub fn set_speed(instance~ : AudioInstance, speed~ : Double) -> Unit {
guard backend.audio_instances.get(instance) is Some(playback) else { return }
let speed = clamp_playback_speed(speed)
match playback.kind {
Sound(sound) => @raylib.set_sound_pitch(sound, to_float(speed))
Music(music) => @raylib.set_music_pitch(music, to_float(speed))
}
}
///|
pub fn set_loop(instance~ : AudioInstance, loop_~ : Bool) -> Unit {
if backend.audio_instances.get(instance) is Some(playback) {
playback.loop_ = loop_
}
}
///|
pub fn set_paused(instance~ : AudioInstance, paused~ : Bool) -> Unit {
guard backend.audio_instances.get(instance) is Some(playback) else { return }
if playback.paused == paused {
return
}
playback.paused = paused
match playback.kind {
Sound(sound) =>
if paused {
if playback.started {
@raylib.pause_sound(sound)
}
} else if playback.started {
@raylib.resume_sound(sound)
} else {
@raylib.play_sound(sound)
playback.started = true
}
Music(music) =>
if paused {
if playback.started {
@raylib.pause_music_stream(music)
}
} else if playback.started {
@raylib.resume_music_stream(music)
} else {
@raylib.play_music_stream(music)
playback.started = true
}
}
}
///|
pub fn stop(instance~ : AudioInstance) -> Unit {
if backend.audio_instances.get(instance) is Some(playback) {
free_audio_playback(playback)
backend.audio_instances.remove(instance)
}
}
///|
pub fn is_finished(instance~ : AudioInstance) -> Bool {
match backend.audio_instances.get(instance) {
Some(playback) => playback.finished
None => true
}
}
///|
pub fn tick_audio() -> Unit {
update_audio_instances()
}
///|
pub fn get_realtime_delta() -> Double {
backend.realtime_delta
}
///|
pub fn preload_img(path : String) -> Unit {
ignore(get_texture(path))
}
///|
pub fn preload_audio(path : String) -> Unit {
ignore(get_sound(path))
}
///|
pub fn set_time_scale(scale : Double) -> Unit {
backend.time_scale = scale
}
///|
pub fn get_time_scale() -> Double {
backend.time_scale
}
///|
pub fn load_font(font : String, path : String) -> Unit {
let loaded = if backend.fonts_by_path.get(path) is Some(loaded) {
loaded
} else {
let loaded = if get_embedded_asset(path) is Some(bytes) {
@raylib.load_font_from_memory(
file_extension(path),
bytes,
bytes.length(),
32,
)
} else {
@raylib.load_font_ex_codepoints(path, 32, default_font_codepoints())
}
backend.fonts_by_path.set(path, loaded)
loaded
}
ignore(loaded)
backend.font_alias.set(font, path)
reset_cosmic_runtime()
}
///|
pub fn set_embedded_assets(lookup : (String) -> Bytes?) -> Unit {
backend.embedded_asset_lookup = lookup
backend.embedded_asset_cache = Map::new()
}
///|
pub fn get_asset_bytes(path : String) -> Bytes? {
if get_embedded_asset(path) is Some(bytes) {
return Some(bytes)
}
if !@raylib.file_exists(path) {
return None
}
Some(@raylib.load_file_data(path))
}
///|
pub fn request_close() -> Unit {
backend.close_requested = true
}