Files
lunarwm/src/LunarWM_render.c
2025-09-29 23:28:39 +03:00

673 lines
20 KiB
C

#include "LunarWM_render.h"
#include "LunarWM_core.h"
#include "LunarWM_xr.h"
#include "common.h"
#include "vec.h"
#include <assert.h>
#include <math.h>
#include <stdlib.h>
#include <time.h>
static inline SphericalCoord get_forward_spherical_with_nearest(
Vector3 fwd, float r)
{
if (fabs(fwd.y) < 0.2f) {
fwd.y = 0;
}
Vector3 vec = Vector3Scale(Vector3Normalize(fwd), r);
return Vector3ToSpherical(vec);
}
static inline Matrix xr_matrix(XrPosef const pose)
{
Matrix const translation
= MatrixTranslate(pose.position.x, pose.position.y, pose.position.z);
Matrix const rotation = QuaternionToMatrix((Quaternion) {
pose.orientation.x,
pose.orientation.y,
pose.orientation.z,
pose.orientation.w,
});
return MatrixMultiply(rotation, translation);
}
static inline Matrix xr_projection_matrix(XrFovf const fov)
{
static_assert(RL_CULL_DISTANCE_FAR > RL_CULL_DISTANCE_NEAR);
Matrix matrix = {};
auto const near = (float)RL_CULL_DISTANCE_NEAR;
auto const far = (float)RL_CULL_DISTANCE_FAR;
float const tan_angle_left = tanf(fov.angleLeft);
float const tan_angle_right = tanf(fov.angleRight);
float const tan_angle_down = tanf(fov.angleDown);
float const tan_angle_up = tanf(fov.angleUp);
float const tan_angle_width = tan_angle_right - tan_angle_left;
float const tan_angle_height = tan_angle_up - tan_angle_down;
matrix.m0 = 2 / tan_angle_width;
matrix.m4 = 0;
matrix.m8 = (tan_angle_right + tan_angle_left) / tan_angle_width;
matrix.m12 = 0;
matrix.m1 = 0;
matrix.m5 = 2 / tan_angle_height;
matrix.m9 = (tan_angle_up + tan_angle_down) / tan_angle_height;
matrix.m13 = 0;
matrix.m2 = 0;
matrix.m6 = 0;
matrix.m10 = -(far + near) / (far - near);
matrix.m14 = -(far * (near + near)) / (far - near);
matrix.m3 = 0;
matrix.m7 = 0;
matrix.m11 = -1;
matrix.m15 = 0;
return matrix;
}
static void DrawBillboardNoShear(
Camera3D const cam, Texture2D tex, Vector3 pos, float scale, Color tint)
{
Rectangle const src = { 0, 0, tex.width, tex.height };
Vector2 const size = { scale * fabsf(src.width / src.height), -scale };
Vector2 const origin = { size.x * 0.5f, size.y * 0.5f };
DrawBillboardPro(cam, tex, src, pos, cam.up, size, origin, 0.0f, tint);
}
void DrawTextureCyl(
Texture2D tex, Vector3 center, float radius, float scale, bool y_flip)
{
if (!tex.id || scale <= 0.0f || radius == 0.0f)
return;
float r = fabsf(radius);
float arc_len = (float)tex.width * scale; // arc length in world units
float theta = arc_len / r; // radians across the panel
float half_t = 0.5f * theta;
float half_h = 0.5f * (float)tex.height * scale;
// mid-angle around Y so the segment's middle sits at 'center'
float a0 = atan2f(center.x, center.z);
// shift so the cylinder surface midpoint matches 'center'
Vector3 mid_ref = (Vector3) { sinf(a0) * r, center.y, cosf(a0) * r };
Vector3 delta = Vector3Subtract(center, mid_ref);
// tessellation: about 3° per slice (min 8)
int slices = (int)ceilf(fmaxf(theta * (180.0f / PI) / 3.0f, 8.0f));
if (slices > 1024)
slices = 1024;
float vt = y_flip ? 1.0f : 0.0f;
float vb = y_flip ? 0.0f : 1.0f;
rlDrawRenderBatchActive(); // flush any prior state
rlSetTexture(tex.id);
rlDisableBackfaceCulling();
rlColor4ub(255, 255, 255, 255);
rlBegin(RL_QUADS);
for (int i = 0; i < slices; ++i) {
float u0 = (float)i / (float)slices;
float u1 = (float)(i + 1) / (float)slices;
float aL = a0 - half_t + theta * u0;
float aR = a0 - half_t + theta * u1;
Vector3 nL = (Vector3) { sinf(aL), 0.0f, cosf(aL) };
Vector3 nR = (Vector3) { sinf(aR), 0.0f, cosf(aR) };
if (radius < 0.0f) {
nL = Vector3Negate(nL);
nR = Vector3Negate(nR);
}
Vector3 pLT = Vector3Add(
(Vector3) { nL.x * r, center.y + half_h, nL.z * r }, delta);
Vector3 pLB = Vector3Add(
(Vector3) { nL.x * r, center.y - half_h, nL.z * r }, delta);
Vector3 pRT = Vector3Add(
(Vector3) { nR.x * r, center.y + half_h, nR.z * r }, delta);
Vector3 pRB = Vector3Add(
(Vector3) { nR.x * r, center.y - half_h, nR.z * r }, delta);
// match your flat-quad U flip (so WL textures look correct)
float U0 = 1.0f - u0;
float U1 = 1.0f - u1;
// one normal per-vertex (simple cylindrical)
rlNormal3f(nL.x, nL.y, nL.z);
rlTexCoord2f(U0, vt);
rlVertex3f(pLT.x, pLT.y, pLT.z);
rlNormal3f(nR.x, nR.y, nR.z);
rlTexCoord2f(U1, vt);
rlVertex3f(pRT.x, pRT.y, pRT.z);
rlNormal3f(nR.x, nR.y, nR.z);
rlTexCoord2f(U1, vb);
rlVertex3f(pRB.x, pRB.y, pRB.z);
rlNormal3f(nL.x, nL.y, nL.z);
rlTexCoord2f(U0, vb);
rlVertex3f(pLB.x, pLB.y, pLB.z);
}
rlEnd();
rlSetTexture(0);
rlEnableBackfaceCulling();
}
static void DrawTextureCyl2(Texture2D tex, Vector3 sphere_center,
SphericalCoord coord, float rad, float scale, bool y_flip)
{
if (!tex.id || scale <= 0.0f || rad == 0.0f)
return;
// midpoint on the sphere where the panel should sit (its center)
Vector3 fwd = SphericalToVector3(coord);
if (Vector3Length(fwd) < 1e-6f)
fwd = (Vector3) { 0, 0, 1 };
fwd = Vector3Normalize(fwd);
// build a local tangent frame at that point (right, up, forward)
Vector3 worldUp = (Vector3) { 0, 1, 0 };
if (fabsf(Vector3DotProduct(worldUp, fwd)) > 0.99f)
worldUp = (Vector3) { 1, 0, 0 };
Vector3 right = Vector3Normalize(Vector3CrossProduct(worldUp, fwd));
Vector3 up = Vector3Normalize(Vector3CrossProduct(fwd, right));
float r = fabsf(rad);
float arc_len = (float)tex.width * scale; // world units across
float theta = arc_len / r; // total horizontal FOV in radians
float half_t = 0.5f * theta;
float half_h = 0.5f * (float)tex.height * scale;
// shift so cylinder's surface midpoint lands exactly at coord.r from
// sphere_center
Vector3 delta = Vector3Add(sphere_center,
Vector3Add(Vector3Scale(fwd, coord.r - r), (Vector3) { 0, 0, 0 }));
// tessellation: about 3° per slice (min 8, max 1024)
int slices = (int)ceilf(fmaxf(theta * (180.0f / PI) / 3.0f, 8.0f));
if (slices > 1024)
slices = 1024;
float vt = y_flip ? 1.0f : 0.0f;
float vb = y_flip ? 0.0f : 1.0f;
rlDrawRenderBatchActive();
rlSetTexture(tex.id);
rlDisableBackfaceCulling();
rlColor4ub(255, 255, 255, 255);
rlBegin(RL_QUADS);
for (int i = 0; i < slices; ++i) {
float u0 = (float)i / (float)slices;
float u1 = (float)(i + 1) / (float)slices;
float aL = -half_t + theta * u0;
float aR = -half_t + theta * u1;
// local outward directions on the cylindrical surface
Vector3 nL = Vector3Add(
Vector3Scale(right, sinf(aL)), Vector3Scale(fwd, cosf(aL)));
Vector3 nR = Vector3Add(
Vector3Scale(right, sinf(aR)), Vector3Scale(fwd, cosf(aR)));
if (rad < 0.0f) {
nL = Vector3Negate(nL);
nR = Vector3Negate(nR);
}
// surface points (center band), then top/bottom by +/- up*half_h
Vector3 cL = Vector3Add(delta, Vector3Scale(nL, r));
Vector3 cR = Vector3Add(delta, Vector3Scale(nR, r));
Vector3 pLT = Vector3Add(cL, Vector3Scale(up, half_h));
Vector3 pLB = Vector3Add(cL, Vector3Scale(up, -half_h));
Vector3 pRT = Vector3Add(cR, Vector3Scale(up, half_h));
Vector3 pRB = Vector3Add(cR, Vector3Scale(up, -half_h));
// match the original horizontal flip so Wayland textures look correct
float U0 = 1.0f - u0;
float U1 = 1.0f - u1;
rlNormal3f(nL.x, nL.y, nL.z);
rlTexCoord2f(U0, vt);
rlVertex3f(pLT.x, pLT.y, pLT.z);
rlNormal3f(nR.x, nR.y, nR.z);
rlTexCoord2f(U1, vt);
rlVertex3f(pRT.x, pRT.y, pRT.z);
rlNormal3f(nR.x, nR.y, nR.z);
rlTexCoord2f(U1, vb);
rlVertex3f(pRB.x, pRB.y, pRB.z);
rlNormal3f(nL.x, nL.y, nL.z);
rlTexCoord2f(U0, vb);
rlVertex3f(pLB.x, pLB.y, pLB.z);
}
rlEnd();
rlSetTexture(0);
rlEnableBackfaceCulling();
}
static inline Vector3 RecenterPoint(LunarWM *wm, Vector3 p)
{
if (!wm->xr.recenter_active)
return p;
return Vector3Add(Vector3RotateByQuaternion(p, wm->xr.recenter_rot),
wm->xr.recenter_trans);
}
static inline Quaternion RecenterOrient(LunarWM *wm, Quaternion q)
{
if (!wm->xr.recenter_active)
return q;
return QuaternionMultiply(wm->xr.recenter_rot, q);
}
static LunarWM_Toplevel *find_toplevel(LunarWM *this, int id)
{
for (size_t i = 0; i < vector_size(this->wayland.v_toplevels); i++) {
auto *tl = this->wayland.v_toplevels[i];
if (tl->id == id)
return tl;
}
return NULL;
}
void LunarWM_render_hud(LunarWM *this, float /*dt*/, int hud_size)
{
ClearBackground((Color) { 0, 0, 0, 0 });
float const text_size = this->cman->cfg.displays.hud.font_size;
char const *txt
= TextFormat("WAYLAND_DISPLAY=%s", getenv("WAYLAND_DISPLAY"));
auto txt_w = MeasureText(txt, 24);
DrawText(
txt, hud_size / 2 - txt_w / 2, hud_size - text_size, text_size, WHITE);
txt = TextFormat("DISPLAY=%s", getenv("DISPLAY"));
txt_w = MeasureText(txt, text_size);
DrawText(txt, hud_size / 2 - txt_w / 2, hud_size - text_size * 2, text_size,
WHITE);
{
time_t t = time(NULL);
struct tm *tm_info = localtime(&t);
int hours = tm_info->tm_hour;
int minutes = tm_info->tm_min;
txt = TextFormat("%02d:%02d", hours, minutes);
txt_w = MeasureText(txt, 32);
DrawText(txt, hud_size / 2 - txt_w / 2, 0, text_size, WHITE);
}
}
void LunarWM_render_windows(LunarWM *this, bool alpha_check)
{
for (size_t i = 0; i
< vector_size(this->wm.workspaces[this->wm.active_workspace].v_windows);
i++) {
auto *window
= &this->wm.workspaces[this->wm.active_workspace].v_windows[i];
auto *tl = window->tl;
if (!tl || !tl->surface) {
continue;
}
if (tl->gles_texture) {
if (alpha_check && tl->composed_has_alpha) {
continue;
}
Texture2D tex = tl->rl_texture;
bool y_flip = false;
if (IsRenderTextureValid(tl->surface_rt)) {
tex = tl->surface_rt.texture;
tex.width = tl->rl_texture.width;
tex.height = tl->rl_texture.height;
y_flip = true;
}
if (!tex.id)
continue;
float rad = window->coord.r - 0.01f * (float)i;
DrawTextureCyl2(tex, Vector3Zero(), window->coord, rad,
this->cman->cfg.space.window_scale, y_flip);
}
}
}
static void render_3d(LunarWM *this, float /*dt*/)
{
LunarWM_render_windows(this, true);
for (int h = 0; h < 2; ++h) {
auto *hand_info = &this->xr.hands[h];
for (size_t k = 0; k < XR_HAND_JOINT_COUNT_EXT; ++k) {
auto const *jl = &hand_info->joint_locations[k];
Vector3 pos = {
jl->pose.position.x,
jl->pose.position.y,
jl->pose.position.z,
};
pos = RecenterPoint(this, pos);
DrawSphere(pos, jl->radius, (Color) { 255, 0, 0, 255 });
}
}
Skybox_draw(this->renderer.skybox, this->renderer.camera.position);
rlEnableColorBlend();
rlDisableDepthMask(); // don't write depth
LunarWM_render_windows(this, false);
// TODO: Replace with actual cursor texture.
{ // Cursor
rlDrawRenderBatchActive();
rlDisableDepthTest();
Vector3 tip = SphericalToVector3(this->wm.pointer);
Vector3 n = Vector3Normalize(
Vector3Subtract(this->renderer.camera.position, tip));
Vector3 up_hint = (Vector3) { 0, 1, 0 };
if (fabsf(Vector3DotProduct(up_hint, n)) > 0.98f)
up_hint = (Vector3) { 1, 0, 0 };
Vector3 right = Vector3Normalize(Vector3CrossProduct(up_hint, n));
Vector3 up = Vector3Normalize(Vector3CrossProduct(n, right));
Vector3 down = Vector3Negate(up);
float const s = 0.03f;
Vector3 v1 = tip;
Vector3 v2 = Vector3Add(tip, Vector3Scale(down, s));
Vector3 v3 = Vector3Add(tip, Vector3Scale(right, s));
Vector3 normal = Vector3CrossProduct(
Vector3Subtract(v2, v1), Vector3Subtract(v3, v1));
if (Vector3DotProduct(normal, n) < 0.0f) {
Vector3 tmp = v2;
v2 = v3;
v3 = tmp;
}
DrawTriangle3D(v1, v2, v3, RED);
rlDrawRenderBatchActive();
rlEnableDepthTest();
}
rlEnableDepthMask();
if (IsTextureValid(this->renderer.hud_rt.texture)) {
rlDrawRenderBatchActive();
rlDisableDepthTest();
Vector3 camPos = this->renderer.camera.position;
Vector3 camDir = Vector3Normalize(
Vector3Subtract(this->renderer.camera.target, camPos));
Vector3 up = this->renderer.camera.up;
Vector3 right = Vector3Normalize(Vector3CrossProduct(camDir, up));
up = Vector3CrossProduct(right, camDir);
Vector3 center = Vector3Add(camPos, Vector3Scale(camDir, 0.6f));
float heightMeters = 0.10f;
DrawBillboardNoShear(this->renderer.camera,
this->renderer.hud_rt.texture, center, heightMeters * 5, WHITE);
rlDrawRenderBatchActive();
rlEnableDepthTest();
}
}
bool LunarWM_render_layer(LunarWM *this, LunarWM_RenderLayerInfo *info, float dt)
{
auto const view_count = (uint32_t)this->xr.view_configuration_views_count;
assert(view_count == 2);
XrView views[2] = {};
for (int i = 0; i < ARRAY_SZ(views); i++) {
views[i] = (XrView) {
.type = XR_TYPE_VIEW,
};
}
XrViewLocateInfo locInfo = {
.type = XR_TYPE_VIEW_LOCATE_INFO,
.viewConfigurationType = XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO,
.displayTime = info->predicted_display_time,
.space = this->xr.local_space,
};
XrViewState viewState = { .type = XR_TYPE_VIEW_STATE };
uint32_t located = 0;
if (xrLocateViews(
this->xr.session, &locInfo, &viewState, view_count, &located, views)
!= XR_SUCCESS
|| located != view_count) {
wlr_log(WLR_ERROR, "Failed to locate views");
return false;
}
// acquire swapchain images
auto *color_sc = &this->xr.swapchains.v_color[0];
auto *depth_sc = &this->xr.swapchains.v_depth[0];
uint32_t col_idx = 0;
uint32_t dep_idx = 0;
if (!LunarWM_xr_acquire_wait(color_sc->swapchain, &col_idx)
|| !LunarWM_xr_acquire_wait(depth_sc->swapchain, &dep_idx)) {
wlr_log(WLR_ERROR, "Swap-chain acquire failed");
return false;
}
GLuint color_tex = LunarWM_xr_get_swapchain_image(this, 0, col_idx);
GLuint depth_tex = LunarWM_xr_get_swapchain_image(this, 1, dep_idx);
// build FBO
if (this->renderer.fbo == 0u) {
glGenFramebuffers(1, &this->renderer.fbo);
}
glBindFramebuffer(GL_FRAMEBUFFER, this->renderer.fbo);
rlFramebufferAttach(this->renderer.fbo, color_tex,
RL_ATTACHMENT_COLOR_CHANNEL0, RL_ATTACHMENT_TEXTURE2D, 0);
rlFramebufferAttach(this->renderer.fbo, depth_tex, RL_ATTACHMENT_DEPTH,
RL_ATTACHMENT_TEXTURE2D, 0);
assert(rlFramebufferComplete(this->renderer.fbo));
uint32_t const eye_w
= this->xr.a_view_configuration_views[0].recommendedImageRectWidth;
uint32_t const eye_h
= this->xr.a_view_configuration_views[0].recommendedImageRectHeight;
this->renderer.tmp_rt = (RenderTexture2D) {
.id = this->renderer.fbo,
.texture = { color_tex, (int)eye_w * view_count, (int)eye_h, 1, -1 },
.depth = { depth_tex, (int)eye_w * view_count, (int)eye_h, 1, -1 },
};
// head-space view matrix (matches rlOpenXR)
XrSpaceLocation headLoc = { .type = XR_TYPE_SPACE_LOCATION };
xrLocateSpace(this->xr.view_space, this->xr.local_space,
info->predicted_display_time, &headLoc);
auto const head_view = MatrixInvert(xr_matrix(headLoc.pose));
// per-eye projection + view-offset
Matrix const view_off_l
= MatrixMultiply(xr_matrix(views[0].pose), head_view);
Matrix const view_off_r
= MatrixMultiply(xr_matrix(views[1].pose), head_view);
Matrix const proj_r = xr_projection_matrix(views[0].fov);
Matrix const proj_l = xr_projection_matrix(views[1].fov);
int const hud_size = this->cman->cfg.displays.hud.size;
if (!IsTextureValid(this->renderer.hud_rt.texture)) {
this->renderer.hud_rt = LoadRenderTexture(hud_size, hud_size);
}
if (IsTextureValid(this->renderer.hud_rt.texture)) {
BeginTextureMode(this->renderer.hud_rt);
{
LunarWM_render_hud(this, dt, hud_size);
}
EndTextureMode();
}
// draw
if (!IsTextureValid(this->renderer.main_rt.texture)) {
this->renderer.main_rt = LoadRenderTexture(eye_w * view_count, eye_h);
}
BeginTextureMode(this->renderer.main_rt);
rlEnableStereoRender();
rlSetMatrixProjectionStereo(proj_r, proj_l);
rlSetMatrixViewOffsetStereo(view_off_r, view_off_l);
glViewport(0, 0, (GLsizei)eye_w * view_count, (GLsizei)eye_h);
rlClearColor(0, 0, 10, 255);
rlClearScreenBuffers();
for (int i = 0; i < 1; i++) {
XrTime const time = info->predicted_display_time;
XrSpaceLocation view_location = { .type = XR_TYPE_SPACE_LOCATION };
XrResult const result = xrLocateSpace(
this->xr.view_space, this->xr.local_space, time, &view_location);
if (result != XR_SUCCESS) {
break;
}
if ((view_location.locationFlags & XR_SPACE_LOCATION_POSITION_VALID_BIT)
!= 0u) {
auto const pos = view_location.pose.position;
this->renderer.camera.position = (Vector3) { pos.x, pos.y, pos.z };
}
if ((view_location.locationFlags
& XR_SPACE_LOCATION_ORIENTATION_VALID_BIT)
!= 0u) {
auto const rot = view_location.pose.orientation;
auto const forward
= Vector3RotateByQuaternion((Vector3) { 0, 0, -1 },
(Quaternion) { rot.x, rot.y, rot.z, rot.w });
auto const up = Vector3RotateByQuaternion((Vector3) { 0, 1, 0 },
(Quaternion) { rot.x, rot.y, rot.z, rot.w });
this->renderer.camera.target
= Vector3Add(this->renderer.camera.position, forward);
this->renderer.camera.up = up;
}
if (this->xr.recenter_active) {
Vector3 pos = this->renderer.camera.position;
Vector3 fwd = Vector3Normalize(
Vector3Subtract(this->renderer.camera.target, pos));
Vector3 up = this->renderer.camera.up;
pos = Vector3Add(
Vector3RotateByQuaternion(pos, this->xr.recenter_rot),
this->xr.recenter_trans);
fwd = Vector3RotateByQuaternion(fwd, this->xr.recenter_rot);
up = Vector3RotateByQuaternion(up, this->xr.recenter_rot);
this->renderer.camera.position = pos;
this->renderer.camera.target = Vector3Add(pos, fwd);
this->renderer.camera.up = up;
}
}
BeginMode3D(this->renderer.camera);
{
ClearBackground(RED);
render_3d(this, dt);
}
EndMode3D();
rlDisableStereoRender();
EndTextureMode();
if (!IsShaderValid(this->renderer.linear_srgb)) {
static char const linear_srgb[] = {
#embed "../assets/linear_srgb.fs"
, 0
};
this->renderer.linear_srgb = LoadShaderFromMemory(NULL, linear_srgb);
}
BeginTextureMode(this->renderer.tmp_rt);
rlDisableColorBlend();
ClearBackground(BLACK);
BeginShaderMode(this->renderer.linear_srgb);
DrawTexturePro(this->renderer.main_rt.texture,
(Rectangle) {
0,
0,
this->renderer.main_rt.texture.width,
-this->renderer.main_rt.texture.height,
},
(Rectangle) {
0,
0,
this->renderer.tmp_rt.texture.width,
this->renderer.tmp_rt.texture.height,
},
(Vector2) { 0, 0 }, 0, WHITE);
EndShaderMode();
EndTextureMode();
rlEnableColorBlend();
// release swapchain images
XrSwapchainImageReleaseInfo const ri
= { .type = XR_TYPE_SWAPCHAIN_IMAGE_RELEASE_INFO };
xrReleaseSwapchainImage(color_sc->swapchain, &ri);
xrReleaseSwapchainImage(depth_sc->swapchain, &ri);
// fill projection layer
info->layer_projection_views_count = view_count;
for (uint32_t i = 0; i < view_count; ++i) {
int32_t const xOff = i * eye_w;
auto *pv = &info->layer_projection_views[i];
pv->pose = views[i].pose;
pv->fov = views[i].fov;
pv->subImage.swapchain = color_sc->swapchain;
pv->subImage.imageRect.offset = (XrOffset2Di) { .x = xOff, .y = 0 };
pv->subImage.imageRect.extent
= (XrExtent2Di) { .width = eye_w, .height = eye_h };
pv->subImage.imageArrayIndex = 0;
}
info->layer_projection = (XrCompositionLayerProjection) {
.type = XR_TYPE_COMPOSITION_LAYER_PROJECTION,
.layerFlags = XR_COMPOSITION_LAYER_BLEND_TEXTURE_SOURCE_ALPHA_BIT
| XR_COMPOSITION_LAYER_CORRECT_CHROMATIC_ABERRATION_BIT,
.space = this->xr.local_space,
.viewCount = view_count,
.views = info->layer_projection_views,
};
info->layers_count = 0;
info->layers[info->layers_count++]
= (XrCompositionLayerBaseHeader *)&info->layer_projection;
if (this->renderer.first_frame) {
LunarWM_set_recenter_from_camera(this);
this->renderer.first_frame = false;
this->wm.pointer = get_forward_spherical_with_nearest(
this->renderer.camera.target, this->cman->cfg.space.radius);
}
return true;
}