diff --git a/doc/changelog.rst b/doc/changelog.rst index 94e6e7f6..f41df670 100644 --- a/doc/changelog.rst +++ b/doc/changelog.rst @@ -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`: 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` everywhere inside it, so that illuminance diff --git a/doc/images/changelog/plane_uv_tiling.png b/doc/images/changelog/plane_uv_tiling.png new file mode 100644 index 00000000..0e0cc6e2 Binary files /dev/null and b/doc/images/changelog/plane_uv_tiling.png differ diff --git a/src/experimental/filament/compat/scene_geom_util.cc b/src/experimental/filament/compat/scene_geom_util.cc index 90f9d455..33b1178b 100644 --- a/src/experimental/filament/compat/scene_geom_util.cc +++ b/src/experimental/filament/compat/scene_geom_util.cc @@ -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(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. diff --git a/src/render/classic/render_context.c b/src/render/classic/render_context.c index ec21204d..985d31d8 100644 --- a/src/render/classic/render_context.c +++ b/src/render/classic/render_context.c @@ -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(); diff --git a/src/render/classic/render_gl3.c b/src/render/classic/render_gl3.c index b5ee76c3..4e1036e4 100644 --- a/src/render/classic/render_gl3.c +++ b/src/render/classic/render_gl3.c @@ -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]) { diff --git a/src/render/filament/assets/pbr.mat b/src/render/filament/assets/pbr.mat index 5da47efc..631da851 100644 --- a/src/render/filament/assets/pbr.mat +++ b/src/render/filament/assets/pbr.mat @@ -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); diff --git a/src/render/filament/assets/pbr_packed.mat b/src/render/filament/assets/pbr_packed.mat index ecd7bfa0..4a192b83 100644 --- a/src/render/filament/assets/pbr_packed.mat +++ b/src/render/filament/assets/pbr_packed.mat @@ -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); diff --git a/src/render/filament/assets/phong_2d.mat b/src/render/filament/assets/phong_2d.mat index 06c873eb..ee6e136f 100644 --- a/src/render/filament/assets/phong_2d.mat +++ b/src/render/filament/assets/phong_2d.mat @@ -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; diff --git a/src/render/filament/assets/phong_2d_fade.mat b/src/render/filament/assets/phong_2d_fade.mat index 15f690e3..d63c226e 100644 --- a/src/render/filament/assets/phong_2d_fade.mat +++ b/src/render/filament/assets/phong_2d_fade.mat @@ -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; diff --git a/src/render/filament/assets/phong_2d_reflect.mat b/src/render/filament/assets/phong_2d_reflect.mat index bbf7ba46..f37e8966 100644 --- a/src/render/filament/assets/phong_2d_reflect.mat +++ b/src/render/filament/assets/phong_2d_reflect.mat @@ -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); diff --git a/src/render/filament/assets/phong_2d_uv.mat b/src/render/filament/assets/phong_2d_uv.mat index 726e9308..491a48fa 100644 --- a/src/render/filament/assets/phong_2d_uv.mat +++ b/src/render/filament/assets/phong_2d_uv.mat @@ -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; diff --git a/src/render/filament/assets/phong_2d_uv_fade.mat b/src/render/filament/assets/phong_2d_uv_fade.mat index bdd6f74e..4419d1c0 100644 --- a/src/render/filament/assets/phong_2d_uv_fade.mat +++ b/src/render/filament/assets/phong_2d_uv_fade.mat @@ -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; diff --git a/src/render/filament/assets/phong_2d_uv_reflect.mat b/src/render/filament/assets/phong_2d_uv_reflect.mat index 4160167d..3d795f8d 100644 --- a/src/render/filament/assets/phong_2d_uv_reflect.mat +++ b/src/render/filament/assets/phong_2d_uv_reflect.mat @@ -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); diff --git a/src/render/filament/core/builtins.cc b/src/render/filament/core/builtins.cc index 9e40f99a..753dba39 100644 --- a/src/render/filament/core/builtins.cc +++ b/src/render/filament/core/builtins.cc @@ -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 positions_; std::vector orientations_; + std::vector texcoords_; std::vector 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(x); const float dy = delta * static_cast(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(x) / num_quads_per_axis_; + const float v = 1.0f - (static_cast(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(num_stacks); - for (int i = 0; i < num_slices; ++i) { - const float angle = static_cast(i) * delta_angle; + for (int i = 0; i <= num_slices; ++i) { + const int geo_i = i % num_slices; + const float angle = static_cast(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(i) / num_slices; for (int j = 0; j <= num_stacks; ++j) { const float z = -1.0f + (static_cast(j) * delta_stack); positions_.emplace_back(pt.x, pt.y, z); orientations_.push_back(orientation); + const float v = 1.0f - ((static_cast(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(num_slices); const float delta_radius = 1.0f / static_cast(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(j) / num_slices; + const float u2 = static_cast(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(j) / num_slices; + const float u2 = static_cast(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(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(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(num_stacks + 1); const float lon_angle_delta = 2.0 * std::numbers::pi / static_cast(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(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; - for (int lon = 0; lon < num_slices; ++lon) { - const float lon_angle = static_cast(lon) * lon_angle_delta; + for (int lon = 0; lon <= num_slices; ++lon) { + const float u = static_cast(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(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(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(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(num_stacks); const float lon_angle_delta = 2.0 * std::numbers::pi / static_cast(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(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); + 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(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(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(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(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(engine, nstack, nslice); tube_ = BuiltinBuilder::Create(engine, nstack, nslice); disk_ = BuiltinBuilder::Create(engine, nslice); - dome_ = BuiltinBuilder::Create(engine, nstack, nslice); + dome_top_ = + BuiltinBuilder::Create(engine, nstack, nslice, false, true); + dome_bottom_ = + BuiltinBuilder::Create(engine, nstack, nslice, true, false); cone_ = BuiltinBuilder::Create(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 diff --git a/src/render/filament/core/builtins.h b/src/render/filament/core/builtins.h index 3024dd1f..4dbbdef6 100644 --- a/src/render/filament/core/builtins.h +++ b/src/render/filament/core/builtins.h @@ -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 sphere_; std::unique_ptr cone_; std::unique_ptr disk_; - std::unique_ptr dome_; + std::unique_ptr dome_top_; + std::unique_ptr dome_bottom_; std::unique_ptr tube_; }; diff --git a/src/render/filament/core/renderable.cc b/src/render/filament/core/renderable.cc index 96dda1be..25cd7fca 100644 --- a/src/render/filament/core/renderable.cc +++ b/src/render/filament/core/renderable.cc @@ -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 diff --git a/src/render/filament/support/model_objects.cc b/src/render/filament/support/model_objects.cc index 3e342c2b..32511ddc 100644 --- a/src/render/filament/support/model_objects.cc +++ b/src/render/filament/support/model_objects.cc @@ -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; } diff --git a/src/render/filament/support/model_renderables.cc b/src/render/filament/support/model_renderables.cc index 948f1a10..764ab421 100644 --- a/src/render/filament/support/model_renderables.cc +++ b/src/render/filament/support/model_renderables.cc @@ -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(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.