VisuTwin Canvas
C++ 3D Engine — Metal Backend
Loading...
Searching...
No Matches
skyMesh.cpp
Go to the documentation of this file.
1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2025-2026 Arnis Lektauers
3//
4// Created by Arnis on 09.10.2025.
5//
6
7#include "skyMesh.h"
8
9#include <cmath>
10#include <cstring>
11
12#include "spdlog/spdlog.h"
17#include "scene/constants.h"
19#include "scene/scene.h"
20
21namespace visutwin::canvas
22{
23 namespace
24 {
25 std::shared_ptr<Mesh> createMesh(const std::shared_ptr<GraphicsDevice>& device,
26 const std::vector<float>& vertices, const std::vector<uint32_t>& indices)
27 {
28 if (!device) {
29 return nullptr;
30 }
31
32 auto mesh = std::make_shared<Mesh>();
33
34 std::vector<uint8_t> vertexBytes(vertices.size() * sizeof(float));
35 std::memcpy(vertexBytes.data(), vertices.data(), vertexBytes.size());
36 VertexBufferOptions vbOptions;
37 vbOptions.data = std::move(vertexBytes);
38 auto vertexFormat = std::make_shared<VertexFormat>(14 * static_cast<int>(sizeof(float)), true, false);
39 auto vertexBuffer = device->createVertexBuffer(vertexFormat, static_cast<int>(vertices.size() / 14), vbOptions);
40
41 std::vector<uint8_t> indexBytes(indices.size() * sizeof(uint32_t));
42 std::memcpy(indexBytes.data(), indices.data(), indexBytes.size());
43 auto indexBuffer = device->createIndexBuffer(INDEXFORMAT_UINT32, static_cast<int>(indices.size()), indexBytes);
44
45 Primitive primitive;
46 primitive.type = PRIMITIVE_TRIANGLES;
47 primitive.base = 0;
48 primitive.baseVertex = 0;
49 primitive.count = static_cast<int>(indices.size());
50 primitive.indexed = true;
51
52 mesh->setVertexBuffer(vertexBuffer);
53 mesh->setIndexBuffer(indexBuffer, 0);
54 mesh->setPrimitive(primitive, 0);
55
56 BoundingBox bounds;
57 bounds.setCenter(0.0f, 0.0f, 0.0f);
58 bounds.setHalfExtents(1.0f, 1.0f, 1.0f);
59 mesh->setAabb(bounds);
60
61 return mesh;
62 }
63
64 std::vector<float> appendVertex(float x, float y, float z)
65 {
66 // position(3), normal(3), uv0(2), tangent(4), uv1(2)
67 return {
68 x, y, z,
69 0.0f, 1.0f, 0.0f,
70 0.0f, 0.0f,
71 1.0f, 0.0f, 0.0f, 1.0f,
72 0.0f, 0.0f
73 };
74 }
75
76 std::shared_ptr<Mesh> createSkyBoxMesh(const std::shared_ptr<GraphicsDevice>& device, const float yOffset)
77 {
78 const float he = 1.0f;
79 const float minY = -he + yOffset;
80 const float maxY = he + yOffset;
81
82 struct Vec3f
83 {
84 float x;
85 float y;
86 float z;
87 };
88
89 const Vec3f corners[8] = {
90 {-he, minY, he},
91 { he, minY, he},
92 { he, maxY, he},
93 {-he, maxY, he},
94 { he, minY, -he},
95 {-he, minY, -he},
96 {-he, maxY, -he},
97 { he, maxY, -he}
98 };
99
100 const int faceAxes[6][3] = {
101 {0, 1, 3}, // FRONT
102 {4, 5, 7}, // BACK
103 {3, 2, 6}, // TOP
104 {1, 0, 4}, // BOTTOM
105 {1, 4, 2}, // RIGHT
106 {5, 0, 6} // LEFT
107 };
108
109 const float faceNormals[6][3] = {
110 { 0.0f, 0.0f, 1.0f}, // FRONT
111 { 0.0f, 0.0f, -1.0f}, // BACK
112 { 0.0f, 1.0f, 0.0f}, // TOP
113 { 0.0f, -1.0f, 0.0f}, // BOTTOM
114 { 1.0f, 0.0f, 0.0f}, // RIGHT
115 {-1.0f, 0.0f, 0.0f} // LEFT
116 };
117
118 std::vector<float> vertices;
119 std::vector<uint32_t> indices;
120 vertices.reserve(6u * 4u * 14u);
121 indices.reserve(6u * 6u);
122
123 uint32_t vcounter = 0;
124 constexpr int uSegments = 1;
125 constexpr int vSegments = 1;
126
127 for (int side = 0; side < 6; ++side) {
128 for (int i = 0; i <= uSegments; ++i) {
129 for (int j = 0; j <= vSegments; ++j) {
130 const float u = static_cast<float>(i) / static_cast<float>(uSegments);
131 const float v = static_cast<float>(j) / static_cast<float>(vSegments);
132
133 const Vec3f c0 = corners[faceAxes[side][0]];
134 const Vec3f c1 = corners[faceAxes[side][1]];
135 const Vec3f c2 = corners[faceAxes[side][2]];
136
137 const Vec3f temp1 = {
138 c0.x + (c1.x - c0.x) * u,
139 c0.y + (c1.y - c0.y) * u,
140 c0.z + (c1.z - c0.z) * u
141 };
142 const Vec3f temp2 = {
143 c0.x + (c2.x - c0.x) * v,
144 c0.y + (c2.y - c0.y) * v,
145 c0.z + (c2.z - c0.z) * v
146 };
147 const Vec3f pos = {
148 temp1.x + (temp2.x - c0.x),
149 temp1.y + (temp2.y - c0.y),
150 temp1.z + (temp2.z - c0.z)
151 };
152
153 vertices.insert(vertices.end(), {
154 pos.x, pos.y, pos.z,
155 faceNormals[side][0], faceNormals[side][1], faceNormals[side][2],
156 u, 1.0f - v,
157 1.0f, 0.0f, 0.0f, 1.0f,
158 0.0f, 0.0f
159 });
160
161 if (i < uSegments && j < vSegments) {
162 indices.push_back(vcounter + static_cast<uint32_t>(vSegments + 1));
163 indices.push_back(vcounter + 1u);
164 indices.push_back(vcounter);
165 indices.push_back(vcounter + static_cast<uint32_t>(vSegments + 1));
166 indices.push_back(vcounter + static_cast<uint32_t>(vSegments + 2));
167 indices.push_back(vcounter + 1u);
168 }
169 vcounter++;
170 }
171 }
172 }
173
174 return createMesh(device, vertices, indices);
175 }
176 }
177
178 SkyMesh::SkyMesh(const std::shared_ptr<GraphicsDevice>& device, Scene* scene,
179 GraphNode* node, Texture* texture, const int type)
180 : _scene(scene)
181 {
182 (void)texture;
183 if (!device || !_scene || !node) {
184 spdlog::warn("SkyMesh: invalid args (device={}, scene={}, node={})", (void*)device.get(), (void*)_scene, (void*)node);
185 return;
186 }
187
188 _mesh = createMeshByType(device, type);
189 if (!_mesh) {
190 spdlog::warn("SkyMesh: createMeshByType returned null for type={}", type);
191 return;
192 }
193
194 auto material = std::make_shared<Material>();
195 material->setName("SkyMaterial");
196 material->setIsSkybox(true);
197 // render inside of sky geometry.
198 material->setCullMode(CullMode::CULLFACE_FRONT);
199 // Skybox must not write to the depth buffer.
200 // Without this, the skybox (rendered at depth ~0.9999) overwrites distant scene geometry
201 // whose depth values exceed 0.9999 due to perspective depth non-linearity.
202 auto skyDepthState = std::make_shared<DepthState>();
203 skyDepthState->setDepthWrite(false);
204 material->setDepthState(skyDepthState);
205 _material = material;
206
207 _meshInstance = std::make_unique<MeshInstance>(_mesh.get(), _material.get(), node);
208
209 auto layers = _scene->layers();
210 if (!layers) {
211 spdlog::warn("SkyMesh: scene has no layers — cannot add sky mesh instance to skybox layer");
212 return;
213 }
214
215 auto skyLayer = layers->getLayerById(LAYERID_SKYBOX);
216 if (skyLayer) {
217 skyLayer->addMeshInstances({_meshInstance.get()});
218 } else {
219 spdlog::warn("SkyMesh: skybox layer (LAYERID_SKYBOX={}) not found", LAYERID_SKYBOX);
220 }
221 }
222
224 {
225 if (_scene && _meshInstance) {
226 auto layers = _scene->layers();
227 if (layers) {
228 auto skyLayer = layers->getLayerById(LAYERID_SKYBOX);
229 if (skyLayer) {
230 skyLayer->removeMeshInstances({_meshInstance.get()});
231 }
232 }
233 }
234 }
235
236 std::shared_ptr<Mesh> SkyMesh::createInfiniteMesh(const std::shared_ptr<GraphicsDevice>& device) const
237 {
238 return createSkyBoxMesh(device, 0.0f);
239 }
240
241 std::shared_ptr<Mesh> SkyMesh::createBoxMesh(const std::shared_ptr<GraphicsDevice>& device) const
242 {
243 // SKYTYPE_BOX uses yOffset: 0.5.
244 return createSkyBoxMesh(device, 0.5f);
245 }
246
247 std::shared_ptr<Mesh> SkyMesh::createDomeMesh(const std::shared_ptr<GraphicsDevice>& device) const
248 {
249 // DomeGeometry extends SphereGeometry.
250 // Full sphere with 50x50 bands, then bottom hemisphere is flattened.
251 constexpr int latitudeBands = 50;
252 constexpr int longitudeBands = 50;
253 constexpr float pi = 3.14159265358979323846f;
254 constexpr float radius = 0.5f;
255 constexpr float bottomLimit = 0.1f;
256 constexpr float curvatureRadius = 0.95f;
257 constexpr float curvatureRadiusSq = curvatureRadius * curvatureRadius;
258
259 // Step 1: Generate full sphere vertices (SphereGeometry)
260 std::vector<float> positions;
261 positions.reserve((latitudeBands + 1) * (longitudeBands + 1) * 3);
262
263 for (int lat = 0; lat <= latitudeBands; ++lat) {
264 const float theta = static_cast<float>(lat) * pi / static_cast<float>(latitudeBands);
265 const float sinTheta = std::sin(theta);
266 const float cosTheta = std::cos(theta);
267
268 for (int lon = 0; lon <= longitudeBands; ++lon) {
269 // sweep from +Z axis (offset by -pi/2)
270 const float phi = static_cast<float>(lon) * 2.0f * pi / static_cast<float>(longitudeBands) - pi * 0.5f;
271 const float sinPhi = std::sin(phi);
272 const float cosPhi = std::cos(phi);
273
274 const float x = cosPhi * sinTheta;
275 const float y = cosTheta;
276 const float z = sinPhi * sinTheta;
277
278 positions.push_back(x * radius);
279 positions.push_back(y * radius);
280 positions.push_back(z * radius);
281 }
282 }
283
284 // Step 2: Flatten bottom hemisphere (DomeGeometry post-processing)
285 for (size_t i = 0; i < positions.size(); i += 3) {
286 const float x = positions[i] / radius;
287 float y = positions[i + 1] / radius;
288 const float z = positions[i + 2] / radius;
289
290 if (y < 0.0f) {
291 // Scale vertices on the bottom
292 y *= 0.3f;
293
294 // Flatten the center
295 if (x * x + z * z < curvatureRadiusSq) {
296 y = -bottomLimit;
297 }
298 }
299
300 // Adjust y to have the center at the flat bottom
301 y += bottomLimit;
302 positions[i + 1] = y * radius;
303 }
304
305 // Step 3: Pack into 14-float vertex format
306 std::vector<float> vertices;
307 vertices.reserve((latitudeBands + 1) * (longitudeBands + 1) * 14);
308
309 for (size_t i = 0; i < positions.size(); i += 3) {
310 const auto packed = appendVertex(positions[i], positions[i + 1], positions[i + 2]);
311 vertices.insert(vertices.end(), packed.begin(), packed.end());
312 }
313
314 // Step 4: Generate indices — Standard winding order
315 std::vector<uint32_t> indices;
316 indices.reserve(latitudeBands * longitudeBands * 6);
317
318 for (int lat = 0; lat < latitudeBands; ++lat) {
319 for (int lon = 0; lon < longitudeBands; ++lon) {
320 const uint32_t first = static_cast<uint32_t>(lat * (longitudeBands + 1) + lon);
321 const uint32_t second = first + static_cast<uint32_t>(longitudeBands + 1);
322
323 // (first+1, second, first) and (first+1, second+1, second)
324 indices.push_back(first + 1);
325 indices.push_back(second);
326 indices.push_back(first);
327 indices.push_back(first + 1);
328 indices.push_back(second + 1);
329 indices.push_back(second);
330 }
331 }
332
333 return createMesh(device, vertices, indices);
334 }
335
336 std::shared_ptr<Mesh> SkyMesh::createSphereMesh(const std::shared_ptr<GraphicsDevice>& device,
337 const int latBands, const int lonBands)
338 {
339 constexpr float pi = 3.14159265358979323846f;
340 // Large radius ensures the sphere extends well beyond the near clip plane.
341 // At globe scale, near clip can be 20+ km; radius must exceed that.
342 // clip.z is overridden to far plane in the vertex shader, so the actual
343 // radius only affects near-plane clipping, not perceived depth.
344 // viewDir = normalize(v.position) is scale-invariant.
345 constexpr float radius = 500000.0f; // 500 km
346
347 std::vector<float> vertices;
348 vertices.reserve((latBands + 1) * (lonBands + 1) * 14);
349
350 for (int lat = 0; lat <= latBands; ++lat) {
351 const float theta = static_cast<float>(lat) * pi / static_cast<float>(latBands);
352 const float sinTheta = std::sin(theta);
353 const float cosTheta = std::cos(theta);
354
355 for (int lon = 0; lon <= lonBands; ++lon) {
356 const float phi = static_cast<float>(lon) * 2.0f * pi / static_cast<float>(lonBands);
357 const float sinPhi = std::sin(phi);
358 const float cosPhi = std::cos(phi);
359
360 const float x = cosPhi * sinTheta * radius;
361 const float y = cosTheta * radius;
362 const float z = sinPhi * sinTheta * radius;
363
364 const auto packed = appendVertex(x, y, z);
365 vertices.insert(vertices.end(), packed.begin(), packed.end());
366 }
367 }
368
369 std::vector<uint32_t> indices;
370 indices.reserve(latBands * lonBands * 6);
371
372 for (int lat = 0; lat < latBands; ++lat) {
373 for (int lon = 0; lon < lonBands; ++lon) {
374 const uint32_t first = static_cast<uint32_t>(lat * (lonBands + 1) + lon);
375 const uint32_t second = first + static_cast<uint32_t>(lonBands + 1);
376
377 indices.push_back(first + 1);
378 indices.push_back(second);
379 indices.push_back(first);
380 indices.push_back(first + 1);
381 indices.push_back(second + 1);
382 indices.push_back(second);
383 }
384 }
385
386 return createMesh(device, vertices, indices);
387 }
388
389 std::shared_ptr<Mesh> SkyMesh::createMeshByType(const std::shared_ptr<GraphicsDevice>& device, const int type) const
390 {
391 switch (type) {
392 case SKYTYPE_BOX:
393 return createBoxMesh(device);
394 case SKYTYPE_DOME:
395 return createDomeMesh(device);
397 return createSphereMesh(device);
398 case SKYTYPE_INFINITE:
399 default:
400 return createInfiniteMesh(device);
401 }
402 }
403} // visutwin
Axis-Aligned Bounding Box defined by center and half-extents.
Definition boundingBox.h:21
void setCenter(const Vector3 &center)
Definition boundingBox.h:28
Hierarchical scene graph node with local/world transforms and parent-child relationships.
Definition graphNode.h:28
Container for the scene graph, lighting environment, fog, skybox, and layer composition.
Definition scene.h:29
static std::shared_ptr< Mesh > createSphereMesh(const std::shared_ptr< GraphicsDevice > &device, int latBands=64, int lonBands=64)
Create a full UV sphere mesh (no flattening). Used for atmosphere sky.
Definition skyMesh.cpp:336
SkyMesh(const std::shared_ptr< GraphicsDevice > &device, Scene *scene, GraphNode *node, Texture *texture, int type)
Definition skyMesh.cpp:178
GPU texture resource supporting 2D, cubemap, volume, and array formats with mipmap management.
Definition texture.h:57
constexpr int LAYERID_SKYBOX
Definition constants.h:21
@ PRIMITIVE_TRIANGLES
Definition mesh.h:23
Describes how vertex and index data should be interpreted for a draw call.
Definition mesh.h:33
PrimitiveType type
Definition mesh.h:34