From b18a8330bd6812e5e5a4f9638033e6afbbab543a Mon Sep 17 00:00:00 2001 From: Adrian Mariano Date: Thu, 10 Apr 2025 22:12:16 -0400 Subject: [PATCH] texture support for vnf_vertex_array --- shapes3d.scad | 22 ++--- skin.scad | 243 ++++++++++++++++++++++++++++++++++++++++++++++---- vnf.scad | 50 ++++++++++- 3 files changed, 282 insertions(+), 33 deletions(-) diff --git a/shapes3d.scad b/shapes3d.scad index 7d9d0ae1..b0056522 100644 --- a/shapes3d.scad +++ b/shapes3d.scad @@ -819,7 +819,8 @@ function prismoid( // You can specify the size of the ends using diameter or radius measured either inside or outside. Alternatively // you can give the length of the side of the polygon. You can specify chamfers and roundings for the ends, but not // the vertical edges. See {{rounded_prism()}} for prisms with rounded vertical edges. You can also specify texture for the side -// faces, but note that texture is not compatible with any roundings or chamfers. +// faces, but note that texture is not compatible with any roundings or chamfers. +// See [Texturing](skin.scad#section-texturing) for more details on how textures work. // . // Anchors are based on the VNF of the prism. Especially for tapered or shifted prisms, this may give unexpected anchor positions, such as top side anchors // being located at the bottom of the shape, so confirm anchor positions before use. @@ -1158,7 +1159,9 @@ function regular_prism(n, // textured_tile(texture, [size], [w1=], [w2=], [ang=], [shift=], [h=/height=/thickness=], [atype=], [diff=], [tex_extra=], [tex_skip=], ...) [ATTACHMENTS]; // vnf = textured_tile(texture, [size], [w1=], [w2=], [ang=], [shift=], [h=/height=/thickness=], [atype=], [tex_extra=], [tex_skip=], ...); // Description: -// Creates a cuboid or trapezoidal prism and places a texture on the top face. You can specify the size by giving a `size` scalar or vector as is +// Creates a cuboid or trapezoidal prism and places a texture on the top face. +// See [Texturing](skin.scad#section-texturing) for more details on how textures work. +// You can specify the size of the object by giving a `size` scalar or vector as is // usual for a cube. If you give a scalar, however, it applies only to the X and Y dimensions: the default is to create a thin tile, not a cube. // The Z size specifies the size of the shape **not** including the applied texture (in the same way that other textured objects work). // If you omit the Z value then for regular textures, the default thickness will be 0.1 which provides a thin backing layer. A zero thickness @@ -1373,14 +1376,9 @@ function textured_tile( height = is_def(size) ? default(size.z,default_thick) : one_defined([h,height,thickness],"h,height,thickness",dflt=default_thick), size = is_def(size) ? is_num(size) ? [size,size,1] : point3d(size,1) // We only use the x and y components of size : [w1,ysize], - - tex = is_string(texture)? texture(texture,$fn=_tex_fn_default()) : texture, - texture = tex_rot==0? tex - : is_vnf(tex)? zrot(tex_rot, cp=[1/2,1/2], p=tex) - : tex_rot==180? reverse([for (row=tex) reverse(row)]) - : tex_rot==270? [for (row=transpose(tex)) reverse(row)] - : reverse(transpose(tex)), - check_tex = _validate_texture(texture), + + texture = _get_texture(texture, tex_rot), + tex_reps = is_def(tex_reps) ? tex_reps : [round(size.x/tex_size.x), round(size.y/tex_size.y)], scale = [size.x/tex_reps.x, size.y/tex_reps.y], @@ -1989,8 +1987,10 @@ function cylinder(h, r1, r2, center, r, d, d1, d2, anchor, spin=0, orient=UP) = // the cylinder or cone's sloped side. The more specific parameters like chamfer1 or rounding2 override the more // general ones like chamfer or rounding, so if you specify `rounding=3, chamfer2=3` you will get a chamfer at the top and // rounding at the bottom. You can specify extra height at either end for use with difference(); the extra height is ignored by -// anchoring. +// anchoring. // . +// You can apply a texture to the cylinder using the usual texture parameters. +// See [Texturing](skin.scad#section-texturing) for more details on how textures work. // When creating a textured cylinder, the number of facets is determined by the sampling of the texture. Any `$fn`, `$fa` or `$fs` values in // effect are ignored. To create a textured prism with a specified number of flat facets use {{regular_prism()}}. Anchors for cylinders // appear on the ideal cylinder, not on actual discretized shape the module produces. For anchors on the shape surface, use {{regular_prism()}}. diff --git a/skin.scad b/skin.scad index f2d4716a..dd9ac30c 100644 --- a/skin.scad +++ b/skin.scad @@ -3150,7 +3150,7 @@ function associate_vertices(polygons, split, curpoly=0) = -// DefineHeader(Table;Headers=Texture Name|Type|Description): Texture Values + // Section: Texturing // Some operations are able to add texture to the objects they create. A texture can be any regularly repeated variation in the height of the surface. @@ -4036,6 +4036,29 @@ function _validate_texture(texture) = true; + +function _tex_height(scale, inset, z) = scale<0 ? -(1-z - inset) * scale + : (z - inset) * scale; + +function _get_texture(texture, tex_rot, extra_rot=0) = + let( + tex_rot=!is_bool(tex_rot)? tex_rot + : echo("boolean value for tex_rot is deprecated. Use a numerical angle divisible by 90.") tex_rot?90:0 + ) + assert(is_num(tex_rot) && posmod(tex_rot,90)==0, "tex_rot must be a multiple of 90 degrees") + let( + tex = is_string(texture)? texture(texture,$fn=_tex_fn_default()) : texture, + check_tex = _validate_texture(tex), + tex_rot = posmod(tex_rot+extra_rot,360), + ) + tex_rot==0 ? tex + : is_vnf(tex)? zrot(tex_rot, cp=[1/2,1/2], p=tex) + : tex_rot==180? reverse([for (row=tex) reverse(row)]) + : tex_rot==270? [for (row=transpose(tex)) reverse(row)] + : reverse(transpose(tex)); + + + function _textured_linear_sweep( region, texture, tex_size=[5,5], h, counts, inset=false, rot=0, @@ -4065,14 +4088,8 @@ function _textured_linear_sweep( caps = is_bool(caps) ? [caps,caps] : caps, regions = is_path(region,2)? [[region]] : region_parts(region), - tex = is_string(texture)? texture(texture,$fn=_tex_fn_default()) : texture, - dummy = assert(is_undef(samples) || is_vnf(tex), "You gave the tex_samples argument with a heightfield texture, which is not permitted. Use the n= argument to texture() instead"), - dummy2=is_bool(rot)?echo("boolean value for tex_rot is deprecated. Use a numerical angle, one of 0, 90, 180, or 270.")0:0, - texture = !rot? tex : - is_vnf(tex)? zrot(is_num(rot)?rot:90, cp=[1/2,1/2], p=tex) : - rot==180? reverse([for (row=tex) reverse(row)]) : - rot==270? [for (row=transpose(tex)) reverse(row)] : - reverse(transpose(tex)), + texture = _get_texture(texture, tex_rot), + dummy = assert(is_undef(samples) || is_vnf(texture), "You gave the tex_samples argument with a heightfield texture, which is not permitted. Use the n= argument to texture() instead"), h = first_defined([h, l, height, length, 1]), inset = is_num(inset)? inset : inset? 1 : 0, twist = default(twist, 0), @@ -4081,7 +4098,6 @@ function _textured_linear_sweep( is_num(scale)? [scale,scale,1] : scale, samples = !is_vnf(texture)? len(texture[0]) : is_num(samples)? samples : 8, - check_tex = _validate_texture(texture), vnf_tile = !is_vnf(texture) || samples==1 ? texture : @@ -4352,15 +4368,8 @@ function _textured_revolution( ) assert(closed || is_path(shape,2)) let( - tex = is_string(texture)? texture(texture,$fn=_tex_fn_default()) : texture, + texture = _get_texture(texture, tex_rot), dummy = assert(is_undef(samples) || is_vnf(tex), "You gave the tex_samples argument with a heightfield texture, which is not permitted. Use the n= argument to texture() instead"), - dummy2=is_bool(rot)?echo("boolean value for tex_rot is deprecated. Use a numerical angle, one of 0, 90, 180, or 270.")0:0, - texture = !rot? tex : - is_vnf(tex)? zrot(is_num(rot)?rot:90, cp=[1/2,1/2], p=tex) : - rot==180? reverse([for (row=tex) reverse(row)]) : - rot==270? [for (row=transpose(tex)) reverse(row)] : - reverse(transpose(tex)), - check_tex = _validate_texture(texture), inset = is_num(inset)? inset : inset? 1 : 0, samples = !is_vnf(texture)? len(texture) : is_num(samples)? samples : 8, @@ -4620,5 +4629,203 @@ module _textured_revolution( } +function _texture_point_array(points, texture, tex_reps, tex_size, tex_samples, tex_inset=false, tex_rot=0, triangulate=false, + col_wrap=false, tex_depth=1, row_wrap=false, caps, cap1, cap2, reverse=false, style="min_edge", tex_extra, tex_skip, sidecaps,sidecap1,sidecap2) = + assert(tex_reps==undef || is_vector(tex_reps,2)) + assert(tex_size==undef || is_num(tex_size) || is_vector(tex_size,2), "tex_size must be a scalar or 2-vector") + assert(num_defined([tex_size, tex_reps])<2, "Cannot give both tex_size and tex_reps") + assert(in_list(style,["default","alt","quincunx", "convex","concave", "min_edge","min_area","flip1","flip2"])) + assert(is_matrix(points[0], n=3),"Point array has the wrong shape or points are not 3d") + assert(is_consistent(points), "Non-rectangular or invalid point array") + let( + cap1 = first_defined([cap1,caps,false]), + cap2 = first_defined([cap2,caps,false]), + sidecap1 = first_defined([sidecap1,sidecaps,false]), + sidecap2 = first_defined([sidecap2,sidecaps,false]), + tex_inset = is_num(tex_inset)? tex_inset : tex_inset? 1 : 0, + texture = _get_texture(texture, tex_rot), + dummy = assert(is_undef(tex_samples) || is_vnf(texture), + "You gave the tex_samples argument with a heightfield texture, which is not permitted. Use the n= argument to texture() instead"), + ptsize=[len(points[0]), len(points)], + tex_reps = is_def(tex_reps) ? tex_reps + : let( + tex_size = is_undef(tex_sizes) ? [5,5] : force_list(tex_size,2), + xsize = norm(points[0][0]-points[0][1])*(ptsize.x+(col_wrap?1:0)), + ysize = norm(points[0][0]-points[1][0])*(ptsize.y+(row_wrap?1:0)) + ) + [round(xsize/tex_size.x), round(ysize/tex_size.y)], + normals = surfnormals(points, col_wrap=col_wrap, row_wrap=row_wrap), + getscale = function(x,y) (x+y)/2 + ) + !is_vnf(texture) ? // heightmap case + let( + extra = is_def(tex_extra) ? force_list(tex_extra,2) + : [col_wrap?0:1, row_wrap?0:1], + skip = is_def(tex_skip) ? force_list(tex_skip,2) : [0,0], + texsize = [len(texture[0]), len(texture)], + fullsize = [texsize.x*tex_reps.x+extra.x-skip.x, texsize.y*tex_reps.y+extra.y-skip.y], + res_points = resample(points,fullsize, col_wrap=col_wrap, row_wrap=row_wrap), + res_normals=resample(normals,fullsize, col_wrap=col_wrap, row_wrap=row_wrap), + local_scale = [for(y=[0:1:fullsize.y-1]) + [for(x=[0:1:fullsize.x-1]) + let( + xlen = [ + if(x>0 || col_wrap) norm(res_points[y][x] - select(res_points[y], x-1)), + if(x0 || row_wrap) norm(res_points[y][x] - select(res_points,y-1)[x]), + if(y0) + [for(x=[0:1:tex_reps.x-1], pt=yedge_paths[0][0]) + trans_pt(x,y,[pt.x,y?0:1,pt.z])], + if (!row_wrap) + for(closed_path=yedge_paths[1], x=[0:1:tex_reps.x-1]) + [for(pt = closed_path) trans_pt(x,y,[pt.x,y?0:1,pt.z])] + ] + ) + for(path=cap_paths) [path, [count(path,reverse=y==0)]], + if (!col_wrap) + for(x=[if (sidecap1) 0, if (sidecap2) tex_reps.x-1]) + let( + cap_paths = [for(closed_path=xedge_paths[1], y=[0:1:tex_reps.y-1]) + [for(pt = closed_path) trans_pt(x,y,[x?1:0,pt.y,pt.z])]] + ) + for(path=cap_paths) [path, [count(path,reverse=x!=0)]] + ]) + ) + reverse ? vnf_reverse_faces(fullvnf) : fullvnf; + +///// These need to be either hidden or documented and placed somewhere. + + +function bilerp(pts,x,y) = + [1,x,y,x*y]*[[1, 0, 0, 0],[-1, 0, 1, 0],[-1,1,0,0],[1,-1,-1,1]]*pts; + + +function resample(data, size, col_wrap=false, row_wrap=false) = + let( + xL=len(data[0]), + yL=len(data), + lastx=xL-(col_wrap?0:1), + lasty=yL-(row_wrap?0:1), + lastoutx = size.x - (col_wrap?0:1), + lastouty = size.y - (row_wrap?0:1), + xscale = lastx/lastoutx, + yscale = lasty/lastouty + ) + [ + for(y=[0:1:lastouty]) + [ + for(x=[0:1:lastoutx]) + let( + sx = xscale*x, + sy = yscale*y, + xind=floor(sx), + yind=floor(sy) + ) + bilerp([data[yind%yL][xind%xL], data[(yind+1)%yL][xind%xL], + data[yind%yL][(xind+1)%xL], data[(yind+1)%yL][(xind+1)%xL]], + sx-xind, sy-yind) + ] + ]; + + + + +function surfnormals(data, col_wrap=false, row_wrap=false) = + let( + rowderivs = [for(y=[0:1:len(data)-1]) path_tangents(data[y],closed=col_wrap)], + colderivs = [for(x=[0:1:len(data[0])-1]) path_tangents(column(data,x), closed=row_wrap)] + ) + [for(y=[0:1:len(data)-1]) + [for(x=[0:1:len(data[0])-1]) + cross(colderivs[x][y],rowderivs[y][x])]]; + + + + // vim: expandtab tabstop=4 shiftwidth=4 softtabstop=4 nowrap diff --git a/vnf.scad b/vnf.scad index d50990a9..c791c130 100644 --- a/vnf.scad +++ b/vnf.scad @@ -48,6 +48,23 @@ EMPTY_VNF = [[],[]]; // The standard empty VNF with no vertices or faces. // adds a vertex in the center of each quadrilateral and creates four triangles, and the "convex" and "concave" styles // choose the locally convex/concave subdivision. The "min_area" option creates the triangulation with the minimal area. Degenerate faces // are not included in the output, but if this results in unused vertices they still appear in the output. +// . +// You can apply a texture to the vertex array VNF using the usual texture parameters. +// See [Texturing](skin.scad#section-texturing) for more details on how textures work. +// The top left corner of the texture tile will be aligned with `points[0][0]`, and the the X and Y directions correspond to `points[y][x]`. +// In practice, it is probably easiest to observe the result and apply a suitable texture tile rotation by setting `tex_rot` if the result +// is not what you wanted. The reference scale of your point data is also taken from the square at the [0][0] corner. This determines +// the meaning of `tex_size` and it also affects the vertical texture scale. The size of the texture tiles will be proportional to the point +// spacing of the location where they are placed, so if the points are closer together, you will get small texture elements. The `tex_depth` you +// specify will be correct at the `points[0][0]` but will be different at places in the point array where the scale is different. Note that this +// differs from {{rotate_sweep()}} which uses a uniform resampling of the curve you specify. +// . +// The point data for `vnf_vertex_array()` is resampled using bilinear interpolation to match the required point density of the tile count, but the +// sampling is based on the grid, not on the distance between points. If you want to +// avoid resampling, match the point data to the required point number for your tile count. For height field textures this means +// the number of data points must equal the tile count times the number of entries in the tile minus `tex_skip` plus `tex_extra`. +// Note that `tex_extra` defaults to 1 along dimensions that are not wrapped. For a VNF tile you need to have the the point +// count equal to the tile count times tex_samples, plus one if wrapping is disabled. // Arguments: // points = A list of vertices to divide into columns and rows. // --- @@ -60,6 +77,18 @@ EMPTY_VNF = [[],[]]; // The standard empty VNF with no vertices or faces. // style = The style of subdividing the quads into faces. Valid options are "default", "alt", "flip1", "flip2", "min_edge", "min_area", "quincunx", "convex" and "concave". // triangulate = If true, triangulates endcaps to resolve possible CGAL issues. This can be an expensive operation if the endcaps are complex. Default: false // convexity = (module) Max number of times a line could intersect a wall of the shape. +// texture = A texture name string, or a rectangular array of scalar height values (0.0 to 1.0), or a VNF tile that defines the texture to apply to vertical surfaces. See {{texture()}} for what named textures are supported. +// tex_size = An optional 2D target size for the textures at `points[0][0]`. Actual texture sizes will be scaled somewhat to evenly fit the available surface. Default: `[5,5]` +// tex_reps = If given instead of tex_size, a 2-vector giving the number of texture tile repetitions in the horizontal and vertical directions. +// tex_inset = If numeric, lowers the texture into the surface by the specified proportion, e.g. 0.5 would lower it half way into the surface. If `true`, insets by exactly its full depth. Default: `false` +// tex_rot = Rotate texture by specified angle, which must be a multiple of 90 degrees. Default: 0 +// tex_depth = Specify texture depth; if negative, invert the texture. Default: 1. +// tex_samples = Minimum number of "bend points" to have in VNF texture tiles. Default: 8 +// tex_extra = number of extra lines of a hightfield texture to add at the end. Can be a scalar or 2-vector to give x and y values. Default: 1 +// tex_skip = number of lines of a heightfield texture to skip when starting. Can be a scalar or two vector to give x and y values. Default: 0 +// sidecaps = if `col_wrap==false` this controls whether to cap any floating ends of a VNF tile on the texture. Does not affect the main texture surface. Ignored it doesn't apply. Default: false +// sidecap1 = set sidecap only for the `points[][0]` edge of the output +// sidecap2 = set sidecap only for the `points[][max]` edge of the output // cp = (module) Centerpoint for determining intersection anchors or centering the shape. Determines the base of the anchor vector. Can be "centroid", "mean", "box" or a 3D point. Default: "centroid" // anchor = (module) Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#subsection-anchor). Default: `"origin"` // spin = (module) Rotate this many degrees around the Z axis after anchor. See [spin](attachments.scad#subsection-spin). Default: `0` @@ -181,10 +210,15 @@ module vnf_vertex_array( reverse=false, style="default", triangulate = false, + texture, tex_reps, tex_size, tex_samples, tex_inset=false, tex_rot=0, + tex_depth=1, tex_extra, tex_skip, sidecaps,sidecap1,sidecap2, convexity=2, cp="centroid", anchor="origin", spin=0, orient=UP, atype="hull") { vnf = vnf_vertex_array(points=points, caps=caps, cap1=cap2, cap2=cap2, - col_wrap=col_wrap, row_wrap=row_wrap, reverse=reverse, style=style,triangulate=triangulate); + col_wrap=col_wrap, row_wrap=row_wrap, reverse=reverse, style=style,triangulate=triangulate, + texture=texture, tex_reps=tex_reps, tex_size=tex_size, tex_samples=tex_samples, tex_inset=tex_inset, tex_rot=tex_rot, + tex_depth=tex_depth, tex_extra=tex_extra, tex_skip=tex_skip, sidecaps=sidecaps,sidecap1=sidecap1,sidecap2=sidecap2 + ); vnf_polyhedron(vnf, convexity=2, cp="centroid", anchor="origin", spin=0, orient=UP, atype="hull") children(); } @@ -196,14 +230,22 @@ function vnf_vertex_array( row_wrap=false, reverse=false, style="default", - triangulate = false + triangulate = false, + texture, tex_reps, tex_size, tex_samples, tex_inset=false, tex_rot=0, + tex_depth=1, tex_extra, tex_skip, sidecaps,sidecap1,sidecap2 ) = - assert(!(any([caps,cap1,cap2]) && !col_wrap), "col_wrap must be true if caps are requested") - assert(!(any([caps,cap1,cap2]) && row_wrap), "Cannot combine caps with row_wrap") assert(in_list(style,["default","alt","quincunx", "convex","concave", "min_edge","min_area","flip1","flip2"])) assert(is_matrix(points[0], n=3),"Point array has the wrong shape or points are not 3d") assert(is_consistent(points), "Non-rectangular or invalid point array") assert(is_bool(triangulate)) + is_def(texture) ? + _texture_point_array(points=points, texture=texture, tex_reps=tex_reps, tex_size=tex_size, + tex_inset=tex_inset, tex_samples=tex_samples, tex_rot=tex_rot, + col_wrap=col_wrap, row_wrap=row_wrap, tex_depth=tex_depth, caps=caps, cap1=cap1, cap2=cap2, reverse=reverse, + style=style, tex_extra=tex_extra, tex_skip=tex_skip, sidecaps=sidecaps, sidecap1=sidecap1, sidecap2=sidecap2,triangulate=triangulate) + : + assert(!(any([caps,cap1,cap2]) && !col_wrap), "col_wrap must be true if caps are requested (without texture)") + assert(!(any([caps,cap1,cap2]) && row_wrap), "Cannot combine caps with row_wrap (without texture)") let( pts = flatten(points), pcnt = len(pts),