Add texture coordinates to primitive shape types.

PiperOrigin-RevId: 964882123
Change-Id: I17723d21f29b3fe28e171de5c3d2bea1f55a94c2
This commit is contained in:
Sam Haves
2026-08-14 14:10:32 -07:00
committed by Copybara-Service
parent 4929f2cd99
commit cc7fb98cf4
18 changed files with 552 additions and 368 deletions
+15
View File
@@ -85,6 +85,21 @@ Rendering
.. admonition:: Breaking API changes
:class: attention
.. image:: https://www.gstatic.com/mujoco/doc/images/changelog/primitives_textured.gif
:align: right
:width: 40%
- Added explicit texture coordinates to built-in geometries (Plane, Box, Sphere, Ellipsoid, Capsule,
Cylinder) in both the Classic renderer and Filament. 2D textures applied to primitive shapes will look different
as textures are mapped using canonical UV parameterizations rather than projecting onto the :math:`x,y` plane.
For finite planes, textures are now anchored to the bottom-left corner instead of the center. This will cause the
most common visual breakage, as common procedural checker textures will be phase shifted. Infinite planes continue
to be anchored at the origin with no visual changes.
.. image:: images/changelog/plane_uv_tiling.png
:align: center
:width: 70%
- Added :ref:`light/softness<body-light-softness>`: edge softness for spotlights under physically-based lighting
models, given as the fraction of the cone over which intensity falls to zero. The default of 0.2 is a semi-soft
cone which delivers the full :ref:`intensity<body-light-intensity>` everywhere inside it, so that illuminance
Binary file not shown.

After

Width:  |  Height:  |  Size: 163 KiB

@@ -28,19 +28,6 @@
namespace mujoco {
// Returns the tile size for infinite plane texture alignment.
// This is duplicated from engine_vis_visualize.c (re-center infinite plane)
// to ensure UV scaling matches the re-centering increments.
static float GetPlaneTileSize(const mjModel* model, int matid,
float texrepeat) {
if (matid >= 0 && texrepeat > 0) {
return 2.0f / texrepeat;
} else {
const float zfar = model->vis.map.zfar * model->stat.extent;
return 2.1f * zfar / (mjMAXPLANEGRID - 2);
}
}
static void PrepareGeomMeshes(mjrfRenderable* renderable, const mjvGeom& geom,
ModelObjects* model_objs,
SceneObjects* scene_objs) {
@@ -230,37 +217,31 @@ static void UpdateGeomMaterial(mjrfRenderable* renderable, const mjvGeom& geom,
}
}
if (tex_uniform) {
if (geom.size[0] > 0) {
material.uv_scale[0] *= geom.size[0];
}
if (geom.size[1] > 0) {
material.uv_scale[1] *= geom.size[1];
}
}
const bool is_infinite_plane =
geom.type == mjGEOM_PLANE && (geom.size[0] <= 0 || geom.size[1] <= 0);
if (is_infinite_plane) {
// Infinite planes are scaled to match the tile size used by
// re-centering in engine_vis_visualize.c.
const float plane_scale = static_cast<float>(mjMAXPLANEGRID) / 2.0f;
const float tile_size_x =
GetPlaneTileSize(model, geom.matid, tex_repeat[0]);
const float tile_size_y =
GetPlaneTileSize(model, geom.matid, tex_repeat[1]);
material.uv_scale[0] = 2.0f * plane_scale / tile_size_x;
material.uv_scale[1] = 2.0f * plane_scale / tile_size_y;
}
// We want to do the equivalent of:
// mjr_setf4(splane, 0.5 * scl.x, 0, 0, -0.5);
// mjr_setf4(tplane, 0, -0.5 * scl.y, 0, -0.5);
// glTexGenfv(GL_S, GL_OBJECT_PLANE, splane);
// glTexGenfv(GL_T, GL_OBJECT_PLANE, tplane);
material.uv_scale[0] = 0.5f * material.uv_scale[0];
material.uv_scale[1] = -0.5f * material.uv_scale[1];
material.uv_offset[0] = -0.5f;
material.uv_offset[1] = -0.5f;
const float dot_pos_axis_x = geom.pos[0] * geom.mat[0] +
geom.pos[1] * geom.mat[3] +
geom.pos[2] * geom.mat[6];
const float dot_pos_axis_y = geom.pos[0] * geom.mat[1] +
geom.pos[1] * geom.mat[4] +
geom.pos[2] * geom.mat[7];
material.uv_scale[0] *= plane_scale;
material.uv_scale[1] *= plane_scale;
// The vertex UVs in PlaneBuilder are u0 = 0.5*x + 0.5, v0 = -0.5*y + 0.5.
// To keep the world-space texture coordinate u = 0.5*worldX*texrepeat - 0.5
// independent of the snapped geomPos, uv_offset must compensate by 0.5*dot_pos.
material.uv_offset[0] =
(0.5f * dot_pos_axis_x - 0.5f * plane_scale) * tex_repeat[0] - 0.5f;
material.uv_offset[1] =
(-0.5f * dot_pos_axis_y - 0.5f * plane_scale) * tex_repeat[1] - 0.5f;
} else if (tex_uniform) {
material.uv_scale[0] *= (geom.size[0] ? geom.size[0] : 1.0f);
material.uv_scale[1] *= (geom.size[1] ? geom.size[1] : 1.0f);
}
} else {
// For cube maps, if `tex_uniform` is true, then scale the texture so that
// it covers a 1x1 area of world space rather than the area of the object.
+181 -120
View File
@@ -117,11 +117,19 @@ static void makePlane(const mjModel* m, mjrContext* con) {
glBegin(GL_QUADS);
glNormal3d(0, 0, 1);
double d = 2.0/m->vis.quality.numquads;
double nq = (double)m->vis.quality.numquads;
for (int x=0; x < m->vis.quality.numquads; x++) {
for (int y=0; y < m->vis.quality.numquads; y++) {
glTexCoord2d((double)x/nq, 1.0 - (double)y/nq);
glVertex3d(d*(x+0)-1, d*(y+0)-1, 0);
glTexCoord2d((double)(x+1)/nq, 1.0 - (double)y/nq);
glVertex3d(d*(x+1)-1, d*(y+0)-1, 0);
glTexCoord2d((double)(x+1)/nq, 1.0 - (double)(y+1)/nq);
glVertex3d(d*(x+1)-1, d*(y+1)-1, 0);
glTexCoord2d((double)x/nq, 1.0 - (double)(y+1)/nq);
glVertex3d(d*(x+0)-1, d*(y+1)-1, 0);
}
}
@@ -198,10 +206,27 @@ static void makePlane(const mjModel* m, mjrContext* con) {
// make grid
for (int x=0; x < nn[0]; x++) {
for (int y=0; y < nn[1]; y++) {
glVertex3d(grid[0][x+0], grid[1][y+0], 0);
glVertex3d(grid[0][x+1], grid[1][y+0], 0);
glVertex3d(grid[0][x+1], grid[1][y+1], 0);
glVertex3d(grid[0][x+0], grid[1][y+1], 0);
double u0, u1, v0, v1;
if (sz[0] > 0) {
u0 = (grid[0][x+0] + sz[0]) / (2.0 * sz[0]);
u1 = (grid[0][x+1] + sz[0]) / (2.0 * sz[0]);
} else {
u0 = 0.5 * grid[0][x+0];
u1 = 0.5 * grid[0][x+1];
}
if (sz[1] > 0) {
v0 = 1.0 - (grid[1][y+0] + sz[1]) / (2.0 * sz[1]);
v1 = 1.0 - (grid[1][y+1] + sz[1]) / (2.0 * sz[1]);
} else {
v0 = -0.5 * grid[1][y+0];
v1 = -0.5 * grid[1][y+1];
}
glTexCoord2d(u0, v0); glVertex3d(grid[0][x+0], grid[1][y+0], 0);
glTexCoord2d(u1, v0); glVertex3d(grid[0][x+1], grid[1][y+0], 0);
glTexCoord2d(u1, v1); glVertex3d(grid[0][x+1], grid[1][y+1], 0);
glTexCoord2d(u0, v1); glVertex3d(grid[0][x+0], grid[1][y+1], 0);
}
}
@@ -552,21 +577,26 @@ static void halfSphere(int sign, int nSlice, int nStack) {
n3[0] = n3[1] = 0;
n3[2] = sign;
float u1 = az1 / (2.0f*mjPI);
float v_el1 = (float)(nStack-1) / (float)nStack;
float u2 = az2 / (2.0f*mjPI);
float u3 = (az1 + az2) / 2.0f / (2.0f*mjPI);
float v3_uv = 1.0f;
if (sign > 0) {
v_el1 = 1.0f - v_el1;
v3_uv = 1.0f - v3_uv;
}
// make triangle
if (sign > 0) {
glNormal3fv(n1);
glVertex3fv(v1);
glNormal3fv(n2);
glVertex3fv(v2);
glNormal3fv(n3);
glVertex3fv(v3);
glTexCoord2f(u1, v_el1); glNormal3fv(n1); glVertex3fv(v1);
glTexCoord2f(u2, v_el1); glNormal3fv(n2); glVertex3fv(v2);
glTexCoord2f(u3, v3_uv); glNormal3fv(n3); glVertex3fv(v3);
} else {
glNormal3fv(n3);
glVertex3fv(v3);
glNormal3fv(n2);
glVertex3fv(v2);
glNormal3fv(n1);
glVertex3fv(v1);
glTexCoord2f(u3, v3_uv); glNormal3fv(n3); glVertex3fv(v3);
glTexCoord2f(u2, v_el1); glNormal3fv(n2); glVertex3fv(v2);
glTexCoord2f(u1, v_el1); glNormal3fv(n1); glVertex3fv(v1);
}
}
glEnd();
@@ -577,6 +607,14 @@ static void halfSphere(int sign, int nSlice, int nStack) {
el1 = (mjPI/2.0f * sign * (i+0)) / (float)nStack;
el2 = (mjPI/2.0f * sign * (i+1)) / (float)nStack;
float v_el1 = (float)(i+0) / (float)nStack;
float v_el2 = (float)(i+1) / (float)nStack;
if (sign > 0) {
v_el1 = 1.0f - v_el1;
v_el2 = 1.0f - v_el2;
}
for (int j=0; j < nSlice; j++) {
az1 = (2.0f*mjPI * (j+0)) / (float)nSlice;
az2 = (2.0f*mjPI * (j+1)) / (float)nSlice;
@@ -587,25 +625,20 @@ static void halfSphere(int sign, int nSlice, int nStack) {
setVertexSphere(v3, n3, az2, el2, sign);
setVertexSphere(v4, n4, az1, el2, sign);
float u1 = az1 / (2.0f*mjPI);
float u2 = az2 / (2.0f*mjPI);
// make quad
if (sign > 0) {
glNormal3fv(n1);
glVertex3fv(v1);
glNormal3fv(n2);
glVertex3fv(v2);
glNormal3fv(n3);
glVertex3fv(v3);
glNormal3fv(n4);
glVertex3fv(v4);
glTexCoord2f(u1, v_el1); glNormal3fv(n1); glVertex3fv(v1);
glTexCoord2f(u2, v_el1); glNormal3fv(n2); glVertex3fv(v2);
glTexCoord2f(u2, v_el2); glNormal3fv(n3); glVertex3fv(v3);
glTexCoord2f(u1, v_el2); glNormal3fv(n4); glVertex3fv(v4);
} else {
glNormal3fv(n4);
glVertex3fv(v4);
glNormal3fv(n3);
glVertex3fv(v3);
glNormal3fv(n2);
glVertex3fv(v2);
glNormal3fv(n1);
glVertex3fv(v1);
glTexCoord2f(u1, v_el2); glNormal3fv(n4); glVertex3fv(v4);
glTexCoord2f(u2, v_el2); glNormal3fv(n3); glVertex3fv(v3);
glTexCoord2f(u2, v_el1); glNormal3fv(n2); glVertex3fv(v2);
glTexCoord2f(u1, v_el1); glNormal3fv(n1); glVertex3fv(v1);
}
}
}
@@ -624,6 +657,9 @@ static void sphere(int nSlice, int nStack) {
glBegin(GL_TRIANGLES);
for (int sign=-1; sign <= 1; sign+=2) {
el1 = (0.5*mjPI * sign * (nStack/2-1)) / (float)(nStack/2);
float v_el1 = 0.5f - sign * (float)(nStack/2-1) / (float)nStack;
float v3_uv = sign > 0 ? 0.0f : 1.0f;
for (int j=0; j < nSlice; j++) {
az1 = (2.0f*mjPI * (j+0.0f)) / (float)nSlice;
az2 = (2.0f*mjPI * (j+1.0f)) / (float)nSlice;
@@ -636,21 +672,19 @@ static void sphere(int nSlice, int nStack) {
n3[0] = n3[1] = 0;
n3[2] = sign;
float u1 = az1 / (2.0f*mjPI);
float u2 = az2 / (2.0f*mjPI);
float u3 = (az1 + az2) / 2.0f / (2.0f*mjPI);
// make triangle
if (sign > 0) {
glNormal3fv(n1);
glVertex3fv(v1);
glNormal3fv(n2);
glVertex3fv(v2);
glNormal3fv(n3);
glVertex3fv(v3);
glTexCoord2f(u1, v_el1); glNormal3fv(n1); glVertex3fv(v1);
glTexCoord2f(u2, v_el1); glNormal3fv(n2); glVertex3fv(v2);
glTexCoord2f(u3, v3_uv); glNormal3fv(n3); glVertex3fv(v3);
} else {
glNormal3fv(n3);
glVertex3fv(v3);
glNormal3fv(n2);
glVertex3fv(v2);
glNormal3fv(n1);
glVertex3fv(v1);
glTexCoord2f(u3, v3_uv); glNormal3fv(n3); glVertex3fv(v3);
glTexCoord2f(u2, v_el1); glNormal3fv(n2); glVertex3fv(v2);
glTexCoord2f(u1, v_el1); glNormal3fv(n1); glVertex3fv(v1);
}
}
}
@@ -663,6 +697,9 @@ static void sphere(int nSlice, int nStack) {
el1 = (0.5*mjPI * sign * (i+0)) / (float)(nStack/2);
el2 = (0.5*mjPI * sign * (i+1)) / (float)(nStack/2);
float v_el1 = 0.5f - sign * (float)(i+0) / (float)nStack;
float v_el2 = 0.5f - sign * (float)(i+1) / (float)nStack;
for (int j=0; j < nSlice; j++) {
az1 = (2.0f*mjPI * (j+0)) / (float)nSlice;
az2 = (2.0f*mjPI * (j+1)) / (float)nSlice;
@@ -673,25 +710,20 @@ static void sphere(int nSlice, int nStack) {
setVertexSphere(v3, n3, az2, el2, 0);
setVertexSphere(v4, n4, az1, el2, 0);
float u1 = az1 / (2.0f*mjPI);
float u2 = az2 / (2.0f*mjPI);
// make quad
if (sign > 0) {
glNormal3fv(n1);
glVertex3fv(v1);
glNormal3fv(n2);
glVertex3fv(v2);
glNormal3fv(n3);
glVertex3fv(v3);
glNormal3fv(n4);
glVertex3fv(v4);
glTexCoord2f(u1, v_el1); glNormal3fv(n1); glVertex3fv(v1);
glTexCoord2f(u2, v_el1); glNormal3fv(n2); glVertex3fv(v2);
glTexCoord2f(u2, v_el2); glNormal3fv(n3); glVertex3fv(v3);
glTexCoord2f(u1, v_el2); glNormal3fv(n4); glVertex3fv(v4);
} else {
glNormal3fv(n4);
glVertex3fv(v4);
glNormal3fv(n3);
glVertex3fv(v3);
glNormal3fv(n2);
glVertex3fv(v2);
glNormal3fv(n1);
glVertex3fv(v1);
glTexCoord2f(u1, v_el2); glNormal3fv(n4); glVertex3fv(v4);
glTexCoord2f(u2, v_el2); glNormal3fv(n3); glVertex3fv(v3);
glTexCoord2f(u2, v_el1); glNormal3fv(n2); glVertex3fv(v2);
glTexCoord2f(u1, v_el1); glNormal3fv(n1); glVertex3fv(v1);
}
}
}
@@ -729,15 +761,22 @@ static void disk(int sign, int nSlice, int nStack) {
v3[0] = v3[1] = 0;
v3[2] = sign;
float u1 = 0.5f + 0.5f * v1[0];
float v1_uv = 0.5f + 0.5f * v1[1];
float u2 = 0.5f + 0.5f * v2[0];
float v2_uv = 0.5f + 0.5f * v2[1];
float u3 = 0.5f;
float v3_uv = 0.5f;
// make triangle
if (sign > 0) {
glVertex3fv(v1);
glVertex3fv(v2);
glVertex3fv(v3);
glTexCoord2f(u1, v1_uv); glVertex3fv(v1);
glTexCoord2f(u2, v2_uv); glVertex3fv(v2);
glTexCoord2f(u3, v3_uv); glVertex3fv(v3);
} else {
glVertex3fv(v3);
glVertex3fv(v2);
glVertex3fv(v1);
glTexCoord2f(u3, v3_uv); glVertex3fv(v3);
glTexCoord2f(u2, v2_uv); glVertex3fv(v2);
glTexCoord2f(u1, v1_uv); glVertex3fv(v1);
}
}
glEnd();
@@ -759,17 +798,26 @@ static void disk(int sign, int nSlice, int nStack) {
setVertexDisk(v3, az2, r1, sign);
setVertexDisk(v4, az1, r1, sign);
float u1 = 0.5f + 0.5f * v1[0];
float v1_uv = 0.5f + 0.5f * v1[1];
float u2 = 0.5f + 0.5f * v2[0];
float v2_uv = 0.5f + 0.5f * v2[1];
float u3 = 0.5f + 0.5f * v3[0];
float v3_uv = 0.5f + 0.5f * v3[1];
float u4 = 0.5f + 0.5f * v4[0];
float v4_uv = 0.5f + 0.5f * v4[1];
// make quad
if (sign > 0) {
glVertex3fv(v1);
glVertex3fv(v2);
glVertex3fv(v3);
glVertex3fv(v4);
glTexCoord2f(u1, v1_uv); glVertex3fv(v1);
glTexCoord2f(u2, v2_uv); glVertex3fv(v2);
glTexCoord2f(u3, v3_uv); glVertex3fv(v3);
glTexCoord2f(u4, v4_uv); glVertex3fv(v4);
} else {
glVertex3fv(v4);
glVertex3fv(v3);
glVertex3fv(v2);
glVertex3fv(v1);
glTexCoord2f(u4, v4_uv); glVertex3fv(v4);
glTexCoord2f(u3, v3_uv); glVertex3fv(v3);
glTexCoord2f(u2, v2_uv); glVertex3fv(v2);
glTexCoord2f(u1, v1_uv); glVertex3fv(v1);
}
}
}
@@ -817,13 +865,14 @@ static void cone(int nSlice, int nStack) {
n3[2] = n1[2]+n2[2];
mjr_normalizeVec(n3);
float u1 = (float)j / (float)nSlice;
float u2 = (float)(j+1) / (float)nSlice;
float u3 = (u1 + u2) / 2.0f;
// make triangle
glNormal3fv(n1);
glVertex3fv(v1);
glNormal3fv(n2);
glVertex3fv(v2);
glNormal3fv(n3);
glVertex3fv(v3);
glTexCoord2f(u1, r1); glNormal3fv(n1); glVertex3fv(v1);
glTexCoord2f(u2, r1); glNormal3fv(n2); glVertex3fv(v2);
glTexCoord2f(u3, 0.0f); glNormal3fv(n3); glVertex3fv(v3);
}
glEnd();
@@ -843,15 +892,14 @@ static void cone(int nSlice, int nStack) {
setVertexCone(v3, n3, az2, r1);
setVertexCone(v4, n4, az1, r1);
float u1 = (float)j / (float)nSlice;
float u2 = (float)(j+1) / (float)nSlice;
// make quad
glNormal3fv(n1);
glVertex3fv(v1);
glNormal3fv(n2);
glVertex3fv(v2);
glNormal3fv(n3);
glVertex3fv(v3);
glNormal3fv(n4);
glVertex3fv(v4);
glTexCoord2f(u1, r2); glNormal3fv(n1); glVertex3fv(v1);
glTexCoord2f(u2, r2); glNormal3fv(n2); glVertex3fv(v2);
glTexCoord2f(u2, r1); glNormal3fv(n3); glVertex3fv(v3);
glTexCoord2f(u1, r1); glNormal3fv(n4); glVertex3fv(v4);
}
}
glEnd();
@@ -883,6 +931,9 @@ static void cylinder(int nSlice, int nStack) {
h1 = 2*(i+0)/(float)nStack - 1;
h2 = 2*(i+1)/(float)nStack - 1;
float v_el1 = (1.0f - h1) / 2.0f;
float v_el2 = (1.0f - h2) / 2.0f;
for (int j=0; j < nSlice; j++) {
az1 = (2.0f*mjPI * (j+0)) / (float)nSlice;
az2 = (2.0f*mjPI * (j+1)) / (float)nSlice;
@@ -893,15 +944,14 @@ static void cylinder(int nSlice, int nStack) {
setVertexCylinder(v3, n3, az2, h2);
setVertexCylinder(v4, n4, az1, h2);
float u1 = (float)j / (float)nSlice;
float u2 = (float)(j+1) / (float)nSlice;
// make quad
glNormal3fv(n1);
glVertex3fv(v1);
glNormal3fv(n2);
glVertex3fv(v2);
glNormal3fv(n3);
glVertex3fv(v3);
glNormal3fv(n4);
glVertex3fv(v4);
glTexCoord2f(u1, v_el1); glNormal3fv(n1); glVertex3fv(v1);
glTexCoord2f(u2, v_el1); glNormal3fv(n2); glVertex3fv(v2);
glTexCoord2f(u2, v_el2); glNormal3fv(n3); glVertex3fv(v3);
glTexCoord2f(u1, v_el2); glNormal3fv(n4); glVertex3fv(v4);
}
}
glEnd();
@@ -1009,43 +1059,54 @@ static void makeBuiltin(const mjModel* m, mjrContext* con) {
// box
glNewList(con->baseBuiltin + mjrBOX, GL_COMPILE);
glBegin(GL_QUADS);
double nq = (double)numquads;
for (int x=0; x < numquads; x++) {
for (int y=0; y < numquads; y++) {
double u0_t = (double)x / nq;
double u1_t = (double)(x+1) / nq;
double v0_t = 1.0 - (double)y / nq;
double v1_t = 1.0 - (double)(y+1) / nq;
double u0_b = (double)x / nq;
double u1_b = (double)(x+1) / nq;
double v0_b = 1.0 - (double)(y+1) / nq;
double v1_b = 1.0 - (double)y / nq;
glNormal3f(0, 0, 1); // top
glVertex3f(d*(x+0)-1, d*(y+0)-1, 1);
glVertex3f(d*(x+1)-1, d*(y+0)-1, 1);
glVertex3f(d*(x+1)-1, d*(y+1)-1, 1);
glVertex3f(d*(x+0)-1, d*(y+1)-1, 1);
glTexCoord2d(u0_t, v0_t); glVertex3f(d*(x+0)-1, d*(y+0)-1, 1);
glTexCoord2d(u1_t, v0_t); glVertex3f(d*(x+1)-1, d*(y+0)-1, 1);
glTexCoord2d(u1_t, v1_t); glVertex3f(d*(x+1)-1, d*(y+1)-1, 1);
glTexCoord2d(u0_t, v1_t); glVertex3f(d*(x+0)-1, d*(y+1)-1, 1);
glNormal3f(0, 0, -1); // bottom
glVertex3f(d*(x+0)-1, d*(y+1)-1, -1);
glVertex3f(d*(x+1)-1, d*(y+1)-1, -1);
glVertex3f(d*(x+1)-1, d*(y+0)-1, -1);
glVertex3f(d*(x+0)-1, d*(y+0)-1, -1);
glTexCoord2d(u0_b, v0_b); glVertex3f(d*(x+0)-1, d*(y+1)-1, -1);
glTexCoord2d(u1_b, v0_b); glVertex3f(d*(x+1)-1, d*(y+1)-1, -1);
glTexCoord2d(u1_b, v1_b); glVertex3f(d*(x+1)-1, d*(y+0)-1, -1);
glTexCoord2d(u0_b, v1_b); glVertex3f(d*(x+0)-1, d*(y+0)-1, -1);
glNormal3f(1, 0, 0); // right
glVertex3f(1, d*(x+0)-1, d*(y+0)-1);
glVertex3f(1, d*(x+1)-1, d*(y+0)-1);
glVertex3f(1, d*(x+1)-1, d*(y+1)-1);
glVertex3f(1, d*(x+0)-1, d*(y+1)-1);
glTexCoord2d(u0_t, v0_t); glVertex3f(1, d*(x+0)-1, d*(y+0)-1);
glTexCoord2d(u1_t, v0_t); glVertex3f(1, d*(x+1)-1, d*(y+0)-1);
glTexCoord2d(u1_t, v1_t); glVertex3f(1, d*(x+1)-1, d*(y+1)-1);
glTexCoord2d(u0_t, v1_t); glVertex3f(1, d*(x+0)-1, d*(y+1)-1);
glNormal3f(-1, 0, 0); // left
glVertex3f(-1, d*(x+0)-1, d*(y+1)-1);
glVertex3f(-1, d*(x+1)-1, d*(y+1)-1);
glVertex3f(-1, d*(x+1)-1, d*(y+0)-1);
glVertex3f(-1, d*(x+0)-1, d*(y+0)-1);
glTexCoord2d(u0_b, v0_b); glVertex3f(-1, d*(x+0)-1, d*(y+1)-1);
glTexCoord2d(u1_b, v0_b); glVertex3f(-1, d*(x+1)-1, d*(y+1)-1);
glTexCoord2d(u1_b, v1_b); glVertex3f(-1, d*(x+1)-1, d*(y+0)-1);
glTexCoord2d(u0_b, v1_b); glVertex3f(-1, d*(x+0)-1, d*(y+0)-1);
glNormal3f(0, -1, 0); // front
glVertex3f(d*(x+0)-1, -1, d*(y+0)-1);
glVertex3f(d*(x+1)-1, -1, d*(y+0)-1);
glVertex3f(d*(x+1)-1, -1, d*(y+1)-1);
glVertex3f(d*(x+0)-1, -1, d*(y+1)-1);
glTexCoord2d(u0_t, v0_t); glVertex3f(d*(x+0)-1, -1, d*(y+0)-1);
glTexCoord2d(u1_t, v0_t); glVertex3f(d*(x+1)-1, -1, d*(y+0)-1);
glTexCoord2d(u1_t, v1_t); glVertex3f(d*(x+1)-1, -1, d*(y+1)-1);
glTexCoord2d(u0_t, v1_t); glVertex3f(d*(x+0)-1, -1, d*(y+1)-1);
glNormal3f(0, 1, 0); // back
glVertex3f(d*(x+0)-1, 1, d*(y+1)-1);
glVertex3f(d*(x+1)-1, 1, d*(y+1)-1);
glVertex3f(d*(x+1)-1, 1, d*(y+0)-1);
glVertex3f(d*(x+0)-1, 1, d*(y+0)-1);
glTexCoord2d(u0_b, v0_b); glVertex3f(d*(x+0)-1, 1, d*(y+1)-1);
glTexCoord2d(u1_b, v0_b); glVertex3f(d*(x+1)-1, 1, d*(y+1)-1);
glTexCoord2d(u1_b, v1_b); glVertex3f(d*(x+1)-1, 1, d*(y+0)-1);
glTexCoord2d(u0_b, v1_b); glVertex3f(d*(x+0)-1, 1, d*(y+0)-1);
}
}
glEnd();
+45 -1
View File
@@ -58,6 +58,21 @@ enum {
};
// check if geom type is a builtin shape with explicit UVs
static int isBuiltinWithUV(int type) {
return (type == mjGEOM_PLANE ||
type == mjGEOM_SPHERE ||
type == mjGEOM_ELLIPSOID ||
type == mjGEOM_BOX ||
type == mjGEOM_CYLINDER ||
type == mjGEOM_CAPSULE ||
type == mjGEOM_ARROW ||
type == mjGEOM_ARROW1 ||
type == mjGEOM_ARROW2 ||
type == mjGEOM_TRIANGLE);
}
// enable/disable texture mapping
static void settexture(int type, int state, const mjrContext* con, const mjvGeom* geom) {
float plane[4], scl[2];
@@ -102,18 +117,44 @@ static void settexture(int type, int state, const mjrContext* con, const mjvGeom
}
// explicit texture coordinates
else if (type == mjtexREGULAR && geom->texcoord) {
else if (type == mjtexREGULAR && geom &&
(geom->texcoord || (texid >= 0 && con->textureType[texid] == mjTEXTURE_2D && isBuiltinWithUV(geom->type)))) {
// enable
if (state && texid >= 0) {
glActiveTexture(GL_TEXTURE0);
glEnable(GL_TEXTURE_2D);
glBindTexture(GL_TEXTURE_2D, con->texture[texid]);
// determine scaling
scl[0] = texrepeat[0] > 0 ? texrepeat[0] : 1.0f;
scl[1] = texrepeat[1] > 0 ? texrepeat[1] : 1.0f;
// uniform: repeat relative to spatial units rather than object
if (texuniform) {
if (geom->size[0] > 0) {
scl[0] = scl[0] * geom->size[0];
}
if (geom->size[1] > 0) {
scl[1] = scl[1] * geom->size[1];
}
}
glMatrixMode(GL_TEXTURE);
glLoadIdentity();
if (geom->type == mjGEOM_PLANE && (geom->size[0] <= 0 || geom->size[1] <= 0)) {
glTranslatef(-0.5f, -0.5f, 0.0f);
}
glScalef(scl[0], scl[1], 1.0f);
glMatrixMode(GL_MODELVIEW);
}
// disable
else {
glActiveTexture(GL_TEXTURE0);
glDisable(GL_TEXTURE_2D);
glMatrixMode(GL_TEXTURE);
glLoadIdentity();
glMatrixMode(GL_MODELVIEW);
}
}
@@ -458,8 +499,11 @@ static void renderGeom(const mjvGeom* geom, int mode, const float* headpos,
glDisable(GL_CULL_FACE);
glBegin(GL_TRIANGLES);
glNormal3f(0, 0, 1);
glTexCoord2f(0, 1);
glVertex3f(0, 0, 0);
glTexCoord2f(1, 1);
glVertex3f(size[0], 0, 0);
glTexCoord2f(0, 0);
glVertex3f(0, size[1], 0);
glEnd();
if (scn->flags[mjRND_CULL_FACE]) {
+4 -2
View File
@@ -26,7 +26,9 @@ material {
{ type : float4, name : BaseColorFactor },
{ type : float, name : MetallicFactor },
{ type : float, name : RoughnessFactor },
{ type : float, name : EmissiveFactor }
{ type : float, name : EmissiveFactor },
{ type : float3, name : UvScale },
{ type : float3, name : UvOffset }
],
requires : [
@@ -36,7 +38,7 @@ material {
fragment {
void material(inout MaterialInputs material) {
vec2 uv = getUV0();
vec2 uv = getUV0() * materialParams.UvScale.xy + materialParams.UvOffset.xy;
material.normal = texture(materialParams_Normal, uv).xyz * 2.0 - 1.0;
prepareMaterial(material);
+4 -2
View File
@@ -24,7 +24,9 @@ material {
{ type : float4, name : BaseColorFactor },
{ type : float, name : MetallicFactor },
{ type : float, name : RoughnessFactor },
{ type : float, name : EmissiveFactor }
{ type : float, name : EmissiveFactor },
{ type : float3, name : UvScale },
{ type : float3, name : UvOffset }
],
requires : [
uv0
@@ -33,7 +35,7 @@ material {
fragment {
void material(inout MaterialInputs material) {
vec2 uv = getUV0();
vec2 uv = getUV0() * materialParams.UvScale.xy + materialParams.UvOffset.xy;
material.normal = texture(materialParams_Normal, uv).xyz * 2.0 - 1.0;
prepareMaterial(material);
+5 -1
View File
@@ -38,7 +38,11 @@ vertex {
fragment {
void material(inout MaterialInputs material) {
vec2 uv = variable_vertex_pos.xy * materialParams.UvScale.xy + materialParams.UvOffset.xy;
// Convert centered object-space positions [-1, 1] to standard [0, 1] UVs:
// * 0.5 scales [-1, 1] to [-0.5, 0.5]
// * -0.5 on Y flips the vertical axis (top of geom = V=0)
// * - vec2(0.5, 0.5) offsets origin to match OpenGL glTexGen conventions
vec2 uv = variable_vertex_pos.xy * vec2(0.5, -0.5) * materialParams.UvScale.xy - vec2(0.5, 0.5) + materialParams.UvOffset.xy;
prepareMaterial(material);
material.baseColor = materialParams.BaseColorFactor;
+5 -1
View File
@@ -39,7 +39,11 @@ vertex {
fragment {
void material(inout MaterialInputs material) {
vec2 uv = variable_vertex_pos.xy * materialParams.UvScale.xy + materialParams.UvOffset.xy;
// Convert centered object-space positions [-1, 1] to standard [0, 1] UVs:
// * 0.5 scales [-1, 1] to [-0.5, 0.5]
// * -0.5 on Y flips the vertical axis (top of geom = V=0)
// * - vec2(0.5, 0.5) offsets origin to match OpenGL glTexGen conventions
vec2 uv = variable_vertex_pos.xy * vec2(0.5, -0.5) * materialParams.UvScale.xy - vec2(0.5, 0.5) + materialParams.UvOffset.xy;
prepareMaterial(material);
material.baseColor = materialParams.BaseColorFactor;
@@ -40,7 +40,11 @@ vertex {
fragment {
void material(inout MaterialInputs material) {
vec2 uv = variable_vertex_pos.xy * materialParams.UvScale.xy + materialParams.UvOffset.xy;
// Convert centered object-space positions [-1, 1] to standard [0, 1] UVs:
// * 0.5 scales [-1, 1] to [-0.5, 0.5]
// * -0.5 on Y flips the vertical axis (top of geom = V=0)
// * - vec2(0.5, 0.5) offsets origin to match OpenGL glTexGen conventions
vec2 uv = variable_vertex_pos.xy * vec2(0.5, -0.5) * materialParams.UvScale.xy - vec2(0.5, 0.5) + materialParams.UvOffset.xy;
vec2 screen_uv = gl_FragCoord.xy * getResolution().zw;
prepareMaterial(material);
+4 -2
View File
@@ -21,7 +21,9 @@ material {
{ type : float, name : SpecularFactor },
{ type : float, name : GlossinessFactor },
{ type : float, name : EmissiveFactor },
{ type : sampler2d, name : BaseColor }
{ type : sampler2d, name : BaseColor },
{ type : float3, name : UvScale },
{ type : float3, name : UvOffset }
],
requires : [
uv0
@@ -30,7 +32,7 @@ material {
fragment {
void material(inout MaterialInputs material) {
vec2 uv = getUV0();
vec2 uv = getUV0() * materialParams.UvScale.xy + materialParams.UvOffset.xy;
prepareMaterial(material);
material.baseColor = materialParams.BaseColorFactor;
@@ -22,7 +22,9 @@ material {
{ type : float, name : SpecularFactor },
{ type : float, name : GlossinessFactor },
{ type : float, name : EmissiveFactor },
{ type : sampler2d, name : BaseColor }
{ type : sampler2d, name : BaseColor },
{ type : float3, name : UvScale },
{ type : float3, name : UvOffset }
],
requires : [
uv0
@@ -31,7 +33,7 @@ material {
fragment {
void material(inout MaterialInputs material) {
vec2 uv = getUV0();
vec2 uv = getUV0() * materialParams.UvScale.xy + materialParams.UvOffset.xy;
prepareMaterial(material);
material.baseColor = materialParams.BaseColorFactor;
@@ -23,7 +23,9 @@ material {
{ type : float, name : EmissiveFactor },
{ type : float, name : Reflectance },
{ type : sampler2d, name : BaseColor },
{ type : sampler2d, name : Reflection }
{ type : sampler2d, name : Reflection },
{ type : float3, name : UvScale },
{ type : float3, name : UvOffset }
],
requires : [
uv0
@@ -32,7 +34,7 @@ material {
fragment {
void material(inout MaterialInputs material) {
vec2 uv = getUV0();
vec2 uv = getUV0() * materialParams.UvScale.xy + materialParams.UvOffset.xy;
vec2 screen_uv = gl_FragCoord.xy * getResolution().zw;
prepareMaterial(material);
+161 -99
View File
@@ -76,17 +76,24 @@ class BuiltinBuilder {
config.max_indices = builder->indices_.size();
config.index_type = mjINDEX_TYPE_U16;
config.primitive_type = builder->primitive_type_;
config.num_attributes = 2;
config.num_attributes = builder->texcoords_.empty() ? 2 : 3;
config.attributes[0].usage = mjVERTEX_ATTRIBUTE_USAGE_POSITION;
config.attributes[0].type = mjVERTEX_ATTRIBUTE_TYPE_FLOAT3;
config.attributes[1].usage = mjVERTEX_ATTRIBUTE_USAGE_TANGENTS;
config.attributes[1].type = mjVERTEX_ATTRIBUTE_TYPE_FLOAT4;
if (!builder->texcoords_.empty()) {
config.attributes[2].usage = mjVERTEX_ATTRIBUTE_USAGE_UV;
config.attributes[2].type = mjVERTEX_ATTRIBUTE_TYPE_FLOAT2;
}
mjrfMeshData data;
mjrf_defaultMeshData(&data);
data.num_vertices = builder->positions_.size();
data.vertices[0] = builder->positions_.data();
data.vertices[1] = builder->orientations_.data();
if (!builder->texcoords_.empty()) {
data.vertices[2] = builder->texcoords_.data();
}
data.num_indices = builder->indices_.size();
data.indices = builder->indices_.data();
data.bounds_min[0] = builder->bounds_min_.x;
@@ -114,6 +121,7 @@ class BuiltinBuilder {
int primitive_type_ = mjMESH_PRIMITIVE_TYPE_TRIANGLES;
std::vector<float3> positions_;
std::vector<float4> orientations_;
std::vector<float2> texcoords_;
std::vector<uint16_t> indices_;
float3 bounds_min_ = {0, 0, 0};
float3 bounds_max_ = {0, 0, 0};
@@ -143,6 +151,7 @@ class PlaneBuilder : public BuiltinBuilder {
explicit PlaneBuilder(int num_quads_per_axis) {
const int num_vertices = NumVerticesPerSide(num_quads_per_axis);
positions_.reserve(num_vertices);
texcoords_.reserve(num_vertices);
const float delta = 2.0f / num_quads_per_axis;
for (int x = 0; x <= num_quads_per_axis; ++x) {
@@ -150,6 +159,7 @@ class PlaneBuilder : public BuiltinBuilder {
const float dx = delta * static_cast<float>(x);
const float dy = delta * static_cast<float>(y);
positions_.emplace_back(dx - 1.0f, dy - 1.0f, 0);
texcoords_.emplace_back(0.5f * dx, 1.0f - (0.5f * dy));
}
}
@@ -180,6 +190,11 @@ class TriangleBuilder : public BuiltinBuilder {
positions_.emplace_back(1, 0, 0);
positions_.emplace_back(0, 1, 0);
texcoords_.reserve(3);
texcoords_.emplace_back(0, 1);
texcoords_.emplace_back(1, 1);
texcoords_.emplace_back(0, 0);
orientations_.resize(positions_.size(), CalculateOrientation({0, 0, 1}));
indices_.reserve(3);
@@ -187,7 +202,7 @@ class TriangleBuilder : public BuiltinBuilder {
indices_.emplace_back(1);
indices_.emplace_back(2);
SetBounds({-1, -1, -0.001}, {1, 1, 0.001});
SetBounds({0, 0, -0.001}, {1, 1, 0.001});
}
};
@@ -255,6 +270,7 @@ class BoxBuilder : public BuiltinBuilder {
positions_.reserve(num_vertices);
orientations_.reserve(num_vertices);
texcoords_.reserve(num_vertices);
indices_.reserve(num_indices);
GenerateVerticesForSide({0, 1, 0},
@@ -302,6 +318,9 @@ class BoxBuilder : public BuiltinBuilder {
const float3 position = pt_gen({dx, dy});
positions_.push_back(position);
orientations_.push_back(orientation);
const float u = static_cast<float>(x) / num_quads_per_axis_;
const float v = 1.0f - (static_cast<float>(y) / num_quads_per_axis_);
texcoords_.emplace_back(u, v);
}
}
}
@@ -313,20 +332,26 @@ class BoxBuilder : public BuiltinBuilder {
class TubeBuilder : public BuiltinBuilder {
public:
TubeBuilder(int num_stacks, int num_slices) {
const int num_vertices = num_slices * (num_stacks + 1);
const int verts_per_ring = num_slices + 1;
const int num_vertices = verts_per_ring * (num_stacks + 1);
positions_.reserve(num_vertices);
orientations_.reserve(num_vertices);
texcoords_.reserve(num_vertices);
const float delta_angle = 2.f * std::numbers::pi / (float)num_slices;
const float delta_stack = 2.f / static_cast<float>(num_stacks);
for (int i = 0; i < num_slices; ++i) {
const float angle = static_cast<float>(i) * delta_angle;
for (int i = 0; i <= num_slices; ++i) {
const int geo_i = i % num_slices;
const float angle = static_cast<float>(geo_i) * delta_angle;
const float2 pt{std::cos(angle), std::sin(angle)};
const float4 orientation = CalculateOrientation({pt.x, pt.y, 0});
const float u = static_cast<float>(i) / num_slices;
for (int j = 0; j <= num_stacks; ++j) {
const float z = -1.0f + (static_cast<float>(j) * delta_stack);
positions_.emplace_back(pt.x, pt.y, z);
orientations_.push_back(orientation);
const float v = 1.0f - ((static_cast<float>(j) * delta_stack) / 2.0f);
texcoords_.emplace_back(u, v);
}
}
@@ -337,10 +362,11 @@ class TubeBuilder : public BuiltinBuilder {
for (int i = 0; i < num_slices; ++i) {
for (int j = 0; j < num_stacks; ++j) {
const int base_idx = (i * num_vertices_in_spine) + j;
const int next_base_idx = ((i + 1) * num_vertices_in_spine) + j;
const int i0 = base_idx + 0;
const int i1 = base_idx + 1;
const int i2 = (base_idx + num_stacks + 2) % num_vertices;
const int i3 = (base_idx + num_stacks + 1) % num_vertices;
const int i2 = next_base_idx + 1;
const int i3 = next_base_idx + 0;
AppendQuadIndices(indices_, i0, i3, i2, i1);
}
}
@@ -357,8 +383,9 @@ class ConeBuilder : public BuiltinBuilder {
((num_stacks - 1) * num_slices * kNumVerticesPerQuad);
positions_.reserve(num_vertices);
orientations_.reserve(num_vertices);
texcoords_.reserve(num_vertices);
// pole: use triangles
// Pole: use triangles
const float delta_angle =
2.0 * std::numbers::pi / static_cast<float>(num_slices);
const float delta_radius = 1.0f / static_cast<float>(num_stacks);
@@ -367,14 +394,20 @@ class ConeBuilder : public BuiltinBuilder {
const float angle1 = (j + 0) * delta_angle;
const float angle2 = (j + 1) * delta_angle;
AppendVert(angle1, delta_radius);
AppendVert(angle2, delta_radius);
const float u1 = static_cast<float>(j) / num_slices;
const float u2 = static_cast<float>(j + 1) / num_slices;
const float v1 = delta_radius;
const float u_mid = (u1 + u2) / 2.f;
AppendVert(angle1, delta_radius, u1, v1);
AppendVert(angle2, delta_radius, u2, v1);
positions_.emplace_back(0, 0, 1);
orientations_.emplace_back(CalculateOrientation({0, 0, 1}));
texcoords_.emplace_back(u_mid, 0.f);
}
// the rest: use quads
// The rest: use quads
for (int i = 1; i < num_stacks; ++i) {
const float radius1 = delta_radius * (i + 0);
const float radius2 = delta_radius * (i + 1);
@@ -382,10 +415,14 @@ class ConeBuilder : public BuiltinBuilder {
for (int j = 0; j < num_slices; ++j) {
const float angle1 = (j + 0) * delta_angle;
const float angle2 = (j + 1) * delta_angle;
AppendVert(angle1, radius2);
AppendVert(angle2, radius2);
AppendVert(angle2, radius1);
AppendVert(angle1, radius1);
const float u1 = static_cast<float>(j) / num_slices;
const float u2 = static_cast<float>(j + 1) / num_slices;
const float v1 = radius1;
const float v2 = radius2;
AppendVert(angle1, radius2, u1, v2);
AppendVert(angle2, radius2, u2, v2);
AppendVert(angle2, radius1, u2, v1);
AppendVert(angle1, radius1, u1, v1);
}
}
@@ -413,7 +450,7 @@ class ConeBuilder : public BuiltinBuilder {
}
private:
void AppendVert(float theta, float radius) {
void AppendVert(float theta, float radius, float u, float v) {
static constexpr float kNormalScale = 0.70710678118f;
const float cz = std::cos(theta);
const float sz = std::sin(theta);
@@ -421,6 +458,7 @@ class ConeBuilder : public BuiltinBuilder {
const float3 n{cz * kNormalScale, sz * kNormalScale, kNormalScale};
positions_.push_back(pt);
orientations_.push_back(CalculateOrientation(n));
texcoords_.emplace_back(u, v);
}
};
@@ -429,15 +467,18 @@ class DiskBuilder : public BuiltinBuilder {
explicit DiskBuilder(int num_slices) {
const int num_vertices = num_slices + 1;
positions_.reserve(num_vertices);
texcoords_.reserve(num_vertices);
const float delta_angle =
2.0 * std::numbers::pi / static_cast<float>(num_slices);
positions_.push_back({0, 0, 0});
texcoords_.emplace_back(0.5f, 0.5f);
for (int i = 0; i < num_slices; ++i) {
const float angle = static_cast<float>(i) * delta_angle;
const float x = std::cos(angle);
const float y = std::sin(angle);
positions_.push_back({x, y, 0});
texcoords_.emplace_back(0.5f + 0.5f * x, 0.5f + 0.5f * y);
}
orientations_.resize(positions_.size(), CalculateOrientation({0, 0, 1}));
@@ -458,178 +499,195 @@ class DiskBuilder : public BuiltinBuilder {
class SphereBuilder : public BuiltinBuilder {
public:
SphereBuilder(int num_stacks, int num_slices) {
static constexpr uint16_t kNorthPoleIndex = 0;
static constexpr uint16_t kSouthPoleIndex = 1;
// To avoid a UV seam artifact, each latitude ring has num_slices+1
// vertices: the last vertex is a geometric duplicate of the first but
// with u=1.0 instead of u=0.0. This prevents the GPU from interpolating
// backwards from u≈0.97 to u=0.0 across the last quad.
//
// Each polar triangle also gets its own pole vertex with u set to the
// midpoint of the two ring vertices, avoiding the degenerate atan2 at
// the pole.
const int num_vertices = (num_stacks * num_slices) + 2; // +2 for poles
const int verts_per_ring = num_slices + 1; // extra vertex for u=1 seam
const int ring_verts = num_stacks * verts_per_ring;
const int pole_verts = 2 * num_slices; // one pole vert per polar triangle
const int num_vertices = ring_verts + pole_verts;
positions_.reserve(num_vertices);
orientations_.reserve(num_vertices);
texcoords_.reserve(num_vertices);
const float lat_angle_delta =
std::numbers::pi / static_cast<float>(num_stacks + 1);
const float lon_angle_delta =
2.0 * std::numbers::pi / static_cast<float>(num_slices);
// Add the north and south poles.
AppendVert(0, 0, 1);
AppendVert(0, 0, -1);
// Vertices by latitude.
// Latitude ring vertices (with seam column).
for (int lat = 0; lat < num_stacks; ++lat) {
// +1 because we handle the north pole (which would be at a lat angle of
// 0-degrees) explicitly.
const float lat_angle = static_cast<float>(lat + 1) * lat_angle_delta;
const float cos_lat_angle = std::cos(lat_angle);
const float sin_lat_angle = std::sin(lat_angle);
const float z = cos_lat_angle;
const float v = lat_angle / std::numbers::pi_v<float>;
for (int lon = 0; lon < num_slices; ++lon) {
const float lon_angle = static_cast<float>(lon) * lon_angle_delta;
for (int lon = 0; lon <= num_slices; ++lon) {
const float u = static_cast<float>(lon) / num_slices;
// Wrap the geometry back to lon=0 for the seam column.
const int geo_lon = lon % num_slices;
const float lon_angle = static_cast<float>(geo_lon) * lon_angle_delta;
const float cos_lon_angle = std::cos(lon_angle);
const float sin_lon_angle = std::sin(lon_angle);
const float x = sin_lat_angle * cos_lon_angle;
const float y = sin_lat_angle * sin_lon_angle;
AppendVert(x, y, z);
const float x = sin_lat_angle * std::cos(lon_angle);
const float y = sin_lat_angle * std::sin(lon_angle);
AppendVert(x, y, z, u, v);
}
}
// Per-face pole vertices. Each polar triangle gets a unique pole vertex
// with u set to the midpoint of the two adjacent ring vertices.
const int north_pole_start = ring_verts;
for (int lon = 0; lon < num_slices; ++lon) {
const float u = (static_cast<float>(lon) + 0.5f) / num_slices;
AppendVert(0, 0, 1, u, 0.0f);
}
const int south_pole_start = north_pole_start + num_slices;
for (int lon = 0; lon < num_slices; ++lon) {
const float u = (static_cast<float>(lon) + 0.5f) / num_slices;
AppendVert(0, 0, -1, u, 1.0f);
}
// Indices.
const size_t num_tris_polar_cap = num_slices;
const size_t num_quads_body = num_slices * (num_stacks - 1);
const int num_indices = (2 * num_tris_polar_cap * kNumIndicesPerTriangle) +
(num_quads_body * kNumIndicesPerQuad);
indices_.reserve(num_indices);
// The first two vertices are the poles, so the first vertex in the first
// row starts at index 2.
uint16_t row_start = kSouthPoleIndex + 1;
// North polar cap.
// North polar cap — each triangle uses its own pole vertex.
const uint16_t first_ring_start = 0;
for (int lon = 0; lon < num_slices; ++lon) {
const int next = lon < (num_slices - 1) ? lon + 1 : 0;
indices_.push_back(kNorthPoleIndex);
indices_.push_back(row_start + lon);
indices_.push_back(row_start + next);
indices_.push_back(north_pole_start + lon);
indices_.push_back(first_ring_start + lon);
indices_.push_back(first_ring_start + lon + 1);
}
// Latitudinal triangle strips.
// Latitudinal quad strips — no index wrapping needed thanks to seam column.
for (int lat = 0; lat < num_stacks - 1; lat++) {
const uint16_t north_start = row_start;
const uint16_t south_start = row_start + num_slices;
const uint16_t north_start = lat * verts_per_ring;
const uint16_t south_start = (lat + 1) * verts_per_ring;
for (int lon = 0; lon < num_slices; ++lon) {
// The offset to the index that is adjacent to the current index.
const int adjacent = lon < (num_slices - 1) ? lon + 1 : 0;
const int i0 = (north_start + lon);
const int i1 = (south_start + lon);
const int i2 = (south_start + adjacent);
const int i3 = (north_start + adjacent);
const int i0 = north_start + lon;
const int i1 = south_start + lon;
const int i2 = south_start + lon + 1;
const int i3 = north_start + lon + 1;
AppendQuadIndices(indices_, i0, i1, i2, i3);
}
row_start += num_slices;
}
// South polar cap.
const uint16_t last_ring_start = (num_stacks - 1) * verts_per_ring;
for (int lon = 0; lon < num_slices; ++lon) {
const int adjacent = lon < (num_slices - 1) ? lon + 1 : 0;
indices_.push_back(kSouthPoleIndex);
indices_.push_back(row_start + adjacent);
indices_.push_back(row_start + lon);
indices_.push_back(south_pole_start + lon);
indices_.push_back(last_ring_start + lon + 1);
indices_.push_back(last_ring_start + lon);
}
SetBounds({-1, -1, -1}, {1, 1, 1});
}
private:
void AppendVert(float x, float y, float z) {
void AppendVert(float x, float y, float z, float u, float v) {
const float3 pt{x, y, z};
positions_.push_back(pt);
orientations_.push_back(CalculateOrientation(pt));
texcoords_.emplace_back(u, v);
}
};
class DomeBuilder : public BuiltinBuilder {
public:
DomeBuilder(int num_stacks, int num_slices) {
static constexpr uint16_t kPoleIndex = 0;
DomeBuilder(int num_stacks, int num_slices, bool flip_u = false,
bool flip_v = false) {
// Same seam-fix strategy as SphereBuilder: extra vertex per ring at u=1.0
// and per-face pole vertices.
const int num_vertices = (num_stacks * num_slices) + 1; // +1 for poles
const int verts_per_ring = num_slices + 1;
const int ring_verts = num_stacks * verts_per_ring;
const int pole_verts = num_slices; // one pole vert per polar triangle
const int num_vertices = ring_verts + pole_verts;
positions_.reserve(num_vertices);
orientations_.reserve(num_vertices);
texcoords_.reserve(num_vertices);
const float lat_angle_delta =
0.5 * std::numbers::pi / static_cast<float>(num_stacks);
const float lon_angle_delta =
2.0 * std::numbers::pi / static_cast<float>(num_slices);
// Add the pole.
AppendVert(0, 0, 1);
// Vertices by latitude.
// Latitude ring vertices (with seam column).
for (int lat = 0; lat < num_stacks; ++lat) {
// +1 because we handle the north pole (which would be at a lat angle of
// 0-degrees) explicitly.
const float lat_angle = static_cast<float>(lat + 1) * lat_angle_delta;
const float cos_lat_angle = std::cos(lat_angle);
const float sin_lat_angle = std::sin(lat_angle);
const float z = cos_lat_angle;
const float v_val = 1.0f - (2.0f * lat_angle / std::numbers::pi_v<float>);
const float v = flip_v ? 1.0f - v_val : v_val;
for (int lon = 0; lon < num_slices; ++lon) {
const float lon_angle = static_cast<float>(lon) * lon_angle_delta;
const float cos_lon_angle = std::cos(lon_angle);
const float sin_lon_angle = std::sin(lon_angle);
for (int lon = 0; lon <= num_slices; ++lon) {
const float u_val = static_cast<float>(lon) / num_slices;
const float u = flip_u ? 1.0f - u_val : u_val;
const int geo_lon = lon % num_slices;
const float lon_angle = static_cast<float>(geo_lon) * lon_angle_delta;
const float x = sin_lat_angle * cos_lon_angle;
const float y = sin_lat_angle * sin_lon_angle;
AppendVert(x, y, z);
const float x = sin_lat_angle * std::cos(lon_angle);
const float y = sin_lat_angle * std::sin(lon_angle);
AppendVert(x, y, z, u, v);
}
}
// Per-face pole vertices.
const int pole_start = ring_verts;
for (int lon = 0; lon < num_slices; ++lon) {
const float u_val = (static_cast<float>(lon) + 0.5f) / num_slices;
const float u = flip_u ? 1.0f - u_val : u_val;
AppendVert(0, 0, 1, u, flip_v ? 0.0f : 1.0f);
}
// Indices.
const size_t num_tris_polar_cap = num_slices;
const size_t num_quads_body = num_slices * (num_stacks - 1);
const int num_indices = (num_tris_polar_cap * kNumIndicesPerTriangle) +
(num_quads_body * kNumIndicesPerQuad);
indices_.reserve(num_indices);
// The first vertex is the poles, so the first vertex in the first row
// starts at index 1.
uint16_t row_start = kPoleIndex + 1;
// North polar cap.
// Polar cap — each triangle uses its own pole vertex.
const uint16_t first_ring_start = 0;
for (int lon = 0; lon < num_slices; ++lon) {
const int next = lon < (num_slices - 1) ? lon + 1 : 0;
indices_.push_back(kPoleIndex);
indices_.push_back(row_start + lon);
indices_.push_back(row_start + next);
indices_.push_back(pole_start + lon);
indices_.push_back(first_ring_start + lon);
indices_.push_back(first_ring_start + lon + 1);
}
// Latitudinal quad strips. The first "stack" was handled above, so we
// only need to iterate over N-1 stacks.
// Latitudinal quad strips.
for (int lat = 0; lat < num_stacks - 1; lat++) {
const int north_start = row_start;
const int south_start = row_start + num_slices;
const uint16_t north_start = lat * verts_per_ring;
const uint16_t south_start = (lat + 1) * verts_per_ring;
for (int lon = 0; lon < num_slices; ++lon) {
// The offset to the index that is adjacent to the current index.
const int adjacent = lon < (num_slices - 1) ? lon + 1 : 0;
const int i0 = (north_start + lon);
const int i1 = (south_start + lon);
const int i2 = (south_start + adjacent);
const int i3 = (north_start + adjacent);
const int i0 = north_start + lon;
const int i1 = south_start + lon;
const int i2 = south_start + lon + 1;
const int i3 = north_start + lon + 1;
AppendQuadIndices(indices_, i0, i1, i2, i3);
}
row_start += num_slices;
}
SetBounds({-1, -1, 0}, {1, 1, 1});
}
private:
void AppendVert(float x, float y, float z) {
void AppendVert(float x, float y, float z, float u, float v) {
const float3 pt{x, y, z};
positions_.push_back(pt);
orientations_.push_back(CalculateOrientation(pt));
texcoords_.emplace_back(u, v);
}
};
@@ -643,7 +701,10 @@ Builtins::Builtins(filament::Engine* engine, int nstack, int nslice,
sphere_ = BuiltinBuilder::Create<SphereBuilder>(engine, nstack, nslice);
tube_ = BuiltinBuilder::Create<TubeBuilder>(engine, nstack, nslice);
disk_ = BuiltinBuilder::Create<DiskBuilder>(engine, nslice);
dome_ = BuiltinBuilder::Create<DomeBuilder>(engine, nstack, nslice);
dome_top_ =
BuiltinBuilder::Create<DomeBuilder>(engine, nstack, nslice, false, true);
dome_bottom_ =
BuiltinBuilder::Create<DomeBuilder>(engine, nstack, nslice, true, false);
cone_ = BuiltinBuilder::Create<ConeBuilder>(engine, nstack, nslice);
}
@@ -655,7 +716,8 @@ const Mesh* Builtins::Box() { return box_.get(); }
const Mesh* Builtins::Sphere() { return sphere_.get(); }
const Mesh* Builtins::Cone() { return cone_.get(); }
const Mesh* Builtins::Disk() { return disk_.get(); }
const Mesh* Builtins::Dome() { return dome_.get(); }
const Mesh* Builtins::DomeTop() { return dome_top_.get(); }
const Mesh* Builtins::DomeBottom() { return dome_bottom_.get(); }
const Mesh* Builtins::Tube() { return tube_.get(); }
} // namespace mujoco
+4 -2
View File
@@ -36,7 +36,8 @@ class Builtins {
const Mesh* Sphere();
const Mesh* Cone();
const Mesh* Disk();
const Mesh* Dome();
const Mesh* DomeTop();
const Mesh* DomeBottom();
const Mesh* Tube();
private:
@@ -48,7 +49,8 @@ class Builtins {
std::unique_ptr<Mesh> sphere_;
std::unique_ptr<Mesh> cone_;
std::unique_ptr<Mesh> disk_;
std::unique_ptr<Mesh> dome_;
std::unique_ptr<Mesh> dome_top_;
std::unique_ptr<Mesh> dome_bottom_;
std::unique_ptr<Mesh> tube_;
};
+6 -40
View File
@@ -170,8 +170,11 @@ void Renderable::UpdateTransform() {
filament::TransformManager& tm = GetEngine()->getTransformManager();
if (geom_type_ == mjGEOM_PLANE && (trs_.size[0] <= 0 || trs_.size[1] <= 0)) {
infinite_plane_ = true;
static constexpr float kInfiniteScale = 0.5f * mjMAXPLANEGRID;
const mat4f scaling =
mat4f::scaling(float3{kInfiniteScale, kInfiniteScale, 1.0f});
const mat4f transform =
filament::math::mat4f(trs_.rotation, trs_.translation);
filament::math::mat4f(trs_.rotation, trs_.translation) * scaling;
for (Part& part : parts_) {
tm.setTransform(tm.getInstance(part.entity), transform);
}
@@ -340,43 +343,6 @@ void Renderable::BindMaterialInstance(const mjrfRenderRequest& request) {
mju_error("No material instances to bind.");
}
if (geom_type_ == mjGEOM_PLANE && infinite_plane_) {
// Emulate an infinite plane by recentering a large quad in world space
// relative to the camera. We use the shared mjMAXPLANEGRID value as the
// size of the quad to ensure the texture scaling matches.
static constexpr float kInfiniteScale = 0.5f * mjMAXPLANEGRID;
const mat4f scaling =
mat4f::scaling(float3{kInfiniteScale, kInfiniteScale, 1.0f});
const float3 camera_pos = ReadFloat3(request.camera.pos);
const float3 plane_origin = transform_[3].xyz;
const mat3f plane_rotation = transform_.upperLeft();
const float3 vec = camera_pos - plane_origin;
const float3 plane_x = normalize(plane_rotation[0]);
const float3 plane_y = normalize(plane_rotation[1]);
// Project camera position onto the plane's local XY axes.
float dx = dot(vec, plane_x);
float dy = dot(vec, plane_y);
// Quantize based on uv_scale.
const float tile_size[] = {kInfiniteScale / material_.uv_scale[0],
kInfiniteScale / material_.uv_scale[1]};
dx = tile_size[0] * mju_round(dx / tile_size[0]);
dy = tile_size[1] * mju_round(dy / tile_size[1]);
// Calculate the new center quad as a displacement from the plane origin.
const float3 displacement = dx * plane_x + dy * plane_y;
const float3 center = plane_origin + displacement;
const mat4f transform = mat4f(plane_rotation, center) * scaling;
filament::TransformManager& tm = GetEngine()->getTransformManager();
for (Part& part : parts_) {
tm.setTransform(tm.getInstance(part.entity), transform);
}
}
const DrawState& state = draw_queue_.front();
SetCastShadows(state.cast_shadows);
SetReceiveShadows(state.receive_shadows);
@@ -502,8 +468,8 @@ void Renderable::SetGeomMesh(mjtGeom type, int nstack, int nslice, int nquad) {
case mjGEOM_CAPSULE:
// Capsules are a tube with two domes at the ends.
AppendMesh(builtins->Tube());
AppendMesh(builtins->Dome());
AppendMesh(builtins->Dome());
AppendMesh(builtins->DomeTop());
AppendMesh(builtins->DomeBottom());
get_transform_fn_ = [](int index, const Trs& trs) {
// We apply an inverse scale to the domes to counteract the capsule's
+43 -17
View File
@@ -157,15 +157,17 @@ static void FillMeshBuffer(MeshBuilder& builder, const mjModel* model, int meshi
static void FillHeightFieldBuffer(MeshBuilder& builder, const mjModel* model,
int hfieldid) {
auto append_tri = [&](float3 a, float3 b, float3 c) {
auto append_tri = [&](float3 a, float3 b, float3 c, float2 uv_a, float2 uv_b,
float2 uv_c) {
float4 orientation = CalculateOrientation(a, b, c);
builder.Append(a, orientation, float2(0, 0));
builder.Append(b, orientation, float2(0, 0));
builder.Append(c, orientation, float2(0, 0));
builder.Append(a, orientation, uv_a);
builder.Append(b, orientation, uv_b);
builder.Append(c, orientation, uv_c);
};
auto append_quad = [&](float3 a, float3 b, float3 c, float3 d) {
append_tri(a, b, d);
append_tri(d, b, c);
auto append_quad = [&](float3 a, float3 b, float3 c, float3 d, float2 uv_a,
float2 uv_b, float2 uv_c, float2 uv_d) {
append_tri(a, b, d, uv_a, uv_b, uv_d);
append_tri(d, b, c, uv_d, uv_b, uv_c);
};
const float* data = model->hfield_data + model->hfield_adr[hfieldid];
@@ -227,10 +229,21 @@ static void FillHeightFieldBuffer(MeshBuilder& builder, const mjModel* model,
}
const float3 mid = {mid_x, mid_y, mid_z};
append_tri(a, b, mid);
append_tri(b, c, mid);
append_tri(c, d, mid);
append_tri(d, a, mid);
const float2 uv_a = {(float)col / (ncol - 1),
1.f - (float)row / (nrow - 1)};
const float2 uv_b = {(float)(col + 1) / (ncol - 1),
1.f - (float)row / (nrow - 1)};
const float2 uv_c = {(float)(col + 1) / (ncol - 1),
1.f - (float)(row + 1) / (nrow - 1)};
const float2 uv_d = {(float)col / (ncol - 1),
1.f - (float)(row + 1) / (nrow - 1)};
const float2 uv_mid = {(float)(col + 0.5f) / (ncol - 1),
1.f - (float)(row + 0.5f) / (nrow - 1)};
append_tri(a, b, mid, uv_a, uv_b, uv_mid);
append_tri(b, c, mid, uv_b, uv_c, uv_mid);
append_tri(c, d, mid, uv_c, uv_d, uv_mid);
append_tri(d, a, mid, uv_d, uv_a, uv_mid);
}
}
// Build the left edge.
@@ -239,7 +252,9 @@ static void FillHeightFieldBuffer(MeshBuilder& builder, const mjModel* model,
const float3 b = get_pos(row + 1, 0);
const float3 c = {b.x, b.y, -sz[3]};
const float3 d = {a.x, a.y, -sz[3]};
append_quad(a, b, c, d);
const float2 uv_a = {0.f, 1.f - (float)row / (nrow - 1)};
const float2 uv_b = {0.f, 1.f - (float)(row + 1) / (nrow - 1)};
append_quad(a, b, c, d, uv_a, uv_b, uv_b, uv_a);
}
// Build the right edge.
for (int row = 0; row < nrow - 1; ++row) {
@@ -247,7 +262,9 @@ static void FillHeightFieldBuffer(MeshBuilder& builder, const mjModel* model,
const float3 b = get_pos(row, ncol - 1);
const float3 c = {b.x, b.y, -sz[3]};
const float3 d = {a.x, a.y, -sz[3]};
append_quad(a, b, c, d);
const float2 uv_a = {1.f, 1.f - (float)(row + 1) / (nrow - 1)};
const float2 uv_b = {1.f, 1.f - (float)row / (nrow - 1)};
append_quad(a, b, c, d, uv_a, uv_b, uv_b, uv_a);
}
// Build the front edge.
for (int col = 0; col < ncol - 1; ++col) {
@@ -255,7 +272,9 @@ static void FillHeightFieldBuffer(MeshBuilder& builder, const mjModel* model,
const float3 b = {a.x, a.y, -sz[3]};
const float3 d = get_pos(0, col + 1);
const float3 c = {d.x, d.y, -sz[3]};
append_quad(a, b, c, d);
const float2 uv_a = {(float)col / (ncol - 1), 1.f};
const float2 uv_d = {(float)(col + 1) / (ncol - 1), 1.f};
append_quad(a, b, c, d, uv_a, uv_a, uv_d, uv_d);
}
// Build the back edge.
for (int col = 0; col < ncol - 1; ++col) {
@@ -263,7 +282,9 @@ static void FillHeightFieldBuffer(MeshBuilder& builder, const mjModel* model,
const float3 b = {a.x, a.y, -sz[3]};
const float3 d = get_pos(nrow - 1, col);
const float3 c = {d.x, d.y, -sz[3]};
append_quad(a, b, c, d);
const float2 uv_a = {(float)(col + 1) / (ncol - 1), 0.f};
const float2 uv_d = {(float)col / (ncol - 1), 0.f};
append_quad(a, b, c, d, uv_a, uv_a, uv_d, uv_d);
}
// Build the base. We use the visualization quality as the size rather than
// the height field dimensions.
@@ -275,8 +296,13 @@ static void FillHeightFieldBuffer(MeshBuilder& builder, const mjModel* model,
const float x1 = sz[0] * ((col + 1) / base_width - 1.0f);
const float y0 = sz[1] * ((row + 0) / base_height - 1.0f);
const float y1 = sz[1] * ((row + 1) / base_height - 1.0f);
const float2 uv0 = {(col + 0) / (2.f * base_width),
1.f - (row + 0) / (2.f * base_height)};
const float2 uv1 = {(col + 1) / (2.f * base_width),
1.f - (row + 1) / (2.f * base_height)};
append_quad({x0, y0, -sz[3]}, {x0, y1, -sz[3]}, {x1, y1, -sz[3]},
{x1, y0, -sz[3]});
{x1, y0, -sz[3]}, {uv0.x, uv0.y}, {uv0.x, uv1.y},
{uv1.x, uv1.y}, {uv1.x, uv0.y});
}
}
}
@@ -303,7 +329,7 @@ static int CalculateHeightFieldVertexCount(const mjModel* model, int hfieldid) {
}
static bool HasUvs(const mjModel* model, int id, MeshType mesh_type) {
return mesh_type != MeshType::kHeightField &&
return mesh_type == MeshType::kHeightField ||
model->mesh_texcoordadr[id] >= 0;
}
@@ -50,19 +50,6 @@ void xtof(float* dst, const T* src, int n) {
}
}
// Returns the tile size for infinite plane texture alignment.
// This is duplicated from engine_vis_visualize.c (re-center infinite plane)
// to ensure UV scaling matches the re-centering increments.
static float GetPlaneTileSize(const mjModel* model, int matid,
float texrepeat) {
if (matid >= 0 && texrepeat > 0) {
return 2.0f / texrepeat;
} else {
const float zfar = model->vis.map.zfar * model->stat.extent;
return 2.1f * zfar / (mjMAXPLANEGRID - 2);
}
}
static mjtCatBit GetBodyCategory(const mjModel* m, int bodyid) {
// mocap subtrees are their own weld, hence not static
if (m->body_weldid[bodyid] == 0) {
@@ -745,12 +732,16 @@ mjrfMaterial ModelRenderables::GetDefaultMaterial(mjtObj obj_type,
mjtGeom geom_type = mjGEOM_NONE;
const float* rgba = nullptr;
const mjtNum* size = nullptr;
const mjtNum* pos = nullptr;
const mjtNum* quat = nullptr;
switch (obj_type) {
case mjOBJ_GEOM:
geom_type = (mjtGeom)model->geom_type[obj_index];
rgba = model->geom_rgba + (4 * obj_index);
size = model->geom_size + (3 * obj_index);
pos = model->geom_pos + (3 * obj_index);
quat = model->geom_quat + (4 * obj_index);
matid = model->geom_matid[obj_index];
break;
case mjOBJ_SITE:
@@ -873,35 +864,49 @@ mjrfMaterial ModelRenderables::GetDefaultMaterial(mjtObj obj_type,
}
}
if (tex_uniform) {
if (fsize[0] > 0) {
material.uv_scale[0] *= fsize[0];
}
if (fsize[1] > 0) {
material.uv_scale[1] *= fsize[1];
}
}
const bool is_infinite_plane =
geom_type == mjGEOM_PLANE && (fsize[0] <= 0 || fsize[1] <= 0);
if (is_infinite_plane) {
// Infinite planes are scaled to match the tile size used by
// re-centering in engine_vis_visualize.c.
// With infinite planes, we want to use the UvOffset to account for
// the plane's position in world space due to re-centering.
//
// Infinite planes will use world-space UVs, with a tile size of 1x1
// world units by default (texrepeat=(1, 1)).
//
// The shader computes: uv = uv0 * UvScale + UvOffset
// We want: uv = dot(worldPos, planeAxis) * texFrequency
//
// Since worldPos = rotation * (objectPos * planeScale) + geomPos:
//
// dot(worldPos, axisK) = objectPos.k * planeScale + dot(geomPos, axisK)
//
// Meaning:
// UvScale = planeScale * texrepeat
// UvOffset = dot(geomPos, planeAxis) * texrepeat
const float plane_scale = static_cast<float>(mjMAXPLANEGRID) / 2.0f;
const float tile_size_x = GetPlaneTileSize(model, matid, tex_repeat[0]);
const float tile_size_y = GetPlaneTileSize(model, matid, tex_repeat[1]);
material.uv_scale[0] = 2.0f * plane_scale / tile_size_x;
material.uv_scale[1] = 2.0f * plane_scale / tile_size_y;
}
// We want to do the equivalent of:
// mjr_setf4(splane, 0.5 * scl.x, 0, 0, -0.5);
// mjr_setf4(tplane, 0, -0.5 * scl.y, 0, -0.5);
// glTexGenfv(GL_S, GL_OBJECT_PLANE, splane);
// glTexGenfv(GL_T, GL_OBJECT_PLANE, tplane);
material.uv_scale[0] = 0.5f * material.uv_scale[0];
material.uv_scale[1] = -0.5f * material.uv_scale[1];
material.uv_offset[0] = -0.5f;
material.uv_offset[1] = -0.5f;
float dot_pos_axis_x = 0.0f;
float dot_pos_axis_y = 0.0f;
if (pos && quat) {
mjtNum mat[9];
mju_quat2Mat(mat, quat);
dot_pos_axis_x = pos[0] * mat[0] + pos[1] * mat[3] + pos[2] * mat[6];
dot_pos_axis_y = pos[0] * mat[1] + pos[1] * mat[4] + pos[2] * mat[7];
}
material.uv_scale[0] *= plane_scale;
material.uv_scale[1] *= plane_scale;
// The vertex UVs in PlaneBuilder are u0 = 0.5*x + 0.5, v0 = -0.5*y + 0.5.
// To keep the world-space texture coordinate u = 0.5*worldX*texrepeat - 0.5
// independent of the snapped geomPos, uv_offset must compensate by 0.5*dot_pos.
material.uv_offset[0] =
(0.5f * dot_pos_axis_x - 0.5f * plane_scale) * tex_repeat[0] - 0.5f;
material.uv_offset[1] =
(-0.5f * dot_pos_axis_y - 0.5f * plane_scale) * tex_repeat[1] - 0.5f;
} else if (tex_uniform) {
material.uv_scale[0] *= (fsize[0] ? fsize[0] : 1.0f);
material.uv_scale[1] *= (fsize[1] ? fsize[1] : 1.0f);
}
} else {
// For cube maps, if `tex_uniform` is true, then scale the texture so that
// it covers a 1x1 area of world space rather than the area of the object.