From 45f2e3f37f22aa7add96279d9dd0369be4456d75 Mon Sep 17 00:00:00 2001 From: Fabio Pellacini Date: Fri, 30 Apr 2021 13:26:50 +0200 Subject: [PATCH] Bug fixes --- .github/workflows/ubuntu-build.yml | 4 +- .vscode/cmake-variants.json | 76 --------- .vscode/settings.json | 3 - CMakePresets.json | 44 +++++ CMakeSettings.json | 28 ---- apps/yshape/yshape.cpp | 112 +++++++++++++ libs/yocto/yocto_image.h | 8 + libs/yocto/yocto_scene.cpp | 76 ++++++--- libs/yocto/yocto_scene.h | 32 ++-- libs/yocto/yocto_sceneio.cpp | 64 +++++++- libs/yocto/yocto_shape.cpp | 240 ++++++++++++++++++++++------ libs/yocto/yocto_shape.h | 37 ++++- tests/shapes1/shapes/matsphere.ply | Bin 0 -> 52592 bytes tests/shapes1/shapes/uvcylinder.ply | Bin 157104 -> 157104 bytes 14 files changed, 525 insertions(+), 199 deletions(-) delete mode 100644 .vscode/cmake-variants.json create mode 100644 CMakePresets.json delete mode 100644 CMakeSettings.json create mode 100644 tests/shapes1/shapes/matsphere.ply diff --git a/.github/workflows/ubuntu-build.yml b/.github/workflows/ubuntu-build.yml index 2fce245b6..64faa4f87 100644 --- a/.github/workflows/ubuntu-build.yml +++ b/.github/workflows/ubuntu-build.yml @@ -12,8 +12,8 @@ jobs: sudo apt-get install xorg-dev - name: configure env: - CC: gcc-8 - CXX: g++-8 + CC: gcc-9 + CXX: g++-9 run: | mkdir build cd build diff --git a/.vscode/cmake-variants.json b/.vscode/cmake-variants.json deleted file mode 100644 index 9073322c8..000000000 --- a/.vscode/cmake-variants.json +++ /dev/null @@ -1,76 +0,0 @@ -{ - "buildType": { - "default": "x64-Release", - "description": "Build type", - "choices": { - "x64-Release": { - "short": "x64-Release", - "buildType": "RelWithDebInfo", - "settings": { - "CMAKE_INSTALL_PREFIX": "${workspaceFolder}/bin" - } - }, - "x64-Debug": { - "short": "x64-Debug", - "buildType": "Debug", - "settings": { - "CMAKE_INSTALL_PREFIX": "${workspaceFolder}/bin/debug" - } - } - } - }, - "useEmbree": { - "default": "yes", - "description": "Build with Embree", - "choices": { - "yes": { - "short": "Embree", - "settings": { - "YOCTO_EMBREE": "yes" - } - }, - "no": { - "short": "NoEmbree", - "settings": { - "YOCTO_EMBREE": "no" - } - } - } - }, - "useDenoise": { - "default": "yes", - "description": "Build with Denoise", - "choices": { - "yes": { - "short": "Denoise", - "settings": { - "YOCTO_DENOISE": "yes" - } - }, - "no": { - "short": "NoDenoise", - "settings": { - "YOCTO_DENOISE": "no" - } - } - } - }, - "useOpenGL": { - "default": "yes", - "description": "Build with OpenGL", - "choices": { - "yes": { - "short": "OpenGL", - "settings": { - "YOCTO_OPENGL": "yes" - } - }, - "no": { - "short": "NoOpenGL", - "settings": { - "YOCTO_OPENGL": "no" - } - } - } - } -} diff --git a/.vscode/settings.json b/.vscode/settings.json index 8e23e3fbd..e88b2552a 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,8 +1,5 @@ { "C_Cpp.default.configurationProvider": "vector-of-bool.cmake-tools", - "cmake.buildDirectory": "${workspaceFolder}/build/vscode/${variant:buildType}", - "cmake.parallelJobs": 8, - "cmake.preferredGenerators": [ "Ninja" ], "files.associations": { "iosfwd": "cpp", "__functional_03": "cpp", diff --git a/CMakePresets.json b/CMakePresets.json new file mode 100644 index 000000000..8d153381d --- /dev/null +++ b/CMakePresets.json @@ -0,0 +1,44 @@ +{ + "version": 2, + "cmakeMinimumRequired": { + "major": 3, + "minor": 20, + "patch": 0 + }, + "configurePresets": [ + { + "name": "release", + "description": "Release build", + "generator": "Ninja", + "binaryDir": "${sourceDir}/build/${presetName}", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "RelWithDebInfo", + "YOCTO_EMBREE": "ON", + "YOCTO_DENOISE": "ON", + "YOCTO_OPENGL": "ON" + } + }, + { + "name": "debug", + "description": "Default build", + "generator": "Ninja", + "binaryDir": "${sourceDir}/build/${presetName}", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Debug", + "YOCTO_EMBREE": "ON", + "YOCTO_DENOISE": "ON", + "YOCTO_OPENGL": "ON" + } + } + ], + "buildPresets": [ + { + "name": "release", + "configurePreset": "release" + }, + { + "name": "debug", + "configurePreset": "debug" + } + ] +} diff --git a/CMakeSettings.json b/CMakeSettings.json deleted file mode 100644 index e9b630a5b..000000000 --- a/CMakeSettings.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "configurations": [ - { - "name": "x64-Debug", - "generator": "Ninja", - "configurationType": "Debug", - "inheritEnvironments": ["msvc_x64_x64"], - "buildRoot": "${projectDir}\\build\\vscmake\\x64-Debug", - "installRoot": "${projectDir}\\bin\\debug", - "cmakeCommandArgs": "", - "buildCommandArgs": "-v", - "ctestCommandArgs": "", - "variables": [{ "name": "YOCTO_EMBREE", "value": "ON" }] - }, - { - "name": "x64-Release", - "generator": "Ninja", - "configurationType": "RelWithDebInfo", - "inheritEnvironments": ["msvc_x64_x64"], - "buildRoot": "${projectDir}\\build\\vscmake\\x64-Release", - "installRoot": "${projectDir}\\bin", - "cmakeCommandArgs": "", - "buildCommandArgs": "-v", - "ctestCommandArgs": "", - "variables": [{ "name": "YOCTO_EMBREE", "value": "ON" }] - } - ] -} diff --git a/apps/yshape/yshape.cpp b/apps/yshape/yshape.cpp index 3d241e7a7..66e21fc37 100644 --- a/apps/yshape/yshape.cpp +++ b/apps/yshape/yshape.cpp @@ -51,6 +51,8 @@ struct convert_params { vec3f rotate = {0, 0, 0}; vec3f scale = {1, 1, 1}; float scaleu = 1; + bool toedges = false; + bool tovertices = false; }; void add_command(cli_command& cli, const string& name, convert_params& params, @@ -73,6 +75,9 @@ void add_command(cli_command& cli, const string& name, convert_params& params, add_option(cmd, "rotatex", params.rotate.x, "Rotate shape."); add_option(cmd, "rotatey", params.rotate.y, "Rotate shape."); add_option(cmd, "rotatez", params.rotate.z, "Rotate shape."); + add_option(cmd, "toedges", params.toedges, "Convert shape to edges."); + add_option( + cmd, "tovertices", params.tovertices, "Convert shape to vertices."); } // convert images @@ -122,6 +127,24 @@ int run_convert(const convert_params& params) { n = transform_normal(xform, n, nonuniform_scaling); } + // convert to edges + if (params.toedges) { + // check faces + if (shape.triangles.empty() && shape.quads.empty()) + print_fatal("empty faces"); + + // convert to edges + auto edges = !shape.triangles.empty() ? get_edges(shape.triangles) + : get_edges(shape.quads); + shape = lines_to_cylinders(edges, shape.positions, 4, 0.001f); + } + + // convert to vertices + if (params.tovertices) { + // convert to spheres + shape = points_to_spheres(shape.positions); + } + // compute normals if (params.smooth) { if (!shape.points.empty()) { @@ -375,6 +398,7 @@ int run_heightfield(const heightfield_params& params) { for (auto& n : shape.normals) n = transform_normal(xform, n, nonuniform_scaling); } + // save mesh if (!save_shape(params.output, shape, ioerror, true)) print_fatal(ioerror); @@ -382,6 +406,86 @@ int run_heightfield(const heightfield_params& params) { return 0; } +struct hair_params { + string shape = "shape.ply"s; + string output = "out.ply"s; + int hairs = 65536; + int steps = 8; + float length = 0.02f; + float noise = 0.001f; + float gravity = 0.0005f; + float radius = 0.0001f; +}; + +void add_command(cli_command& cli, const string& name, hair_params& params, + const string& usage) { + auto& cmd = add_command(cli, name, usage); + add_argument(cmd, "shape", params.shape, "Input shape."); + add_option(cmd, "output", params.output, "Output shape."); + add_option(cmd, "hairs", params.hairs, "Number of hairs."); + add_option(cmd, "steps", params.steps, "Hair steps."); + add_option(cmd, "length", params.length, "Hair length."); + add_option(cmd, "noise", params.noise, "Noise weight."); + add_option(cmd, "gravity", params.gravity, "Gravity scale."); + add_option(cmd, "radius", params.radius, "Hair radius."); +} + +int run_hair(const hair_params& params) { + // load mesh + auto shape = scene_shape{}; + auto ioerror = ""s; + if (!load_shape(params.shape, shape, ioerror)) print_fatal(ioerror); + + // generate hair + auto hair = make_hair2(shape, {params.steps, params.hairs}, + {params.length, params.length}, {params.radius, params.radius}, + params.noise, params.gravity); + + // save mesh + if (!save_shape(params.output, hair, ioerror, true)) print_fatal(ioerror); + + // done + return 0; +} + +struct sample_params { + string shape = "shape.ply"s; + string output = "out.ply"s; + int samples = 4096; +}; + +void add_command(cli_command& cli, const string& name, sample_params& params, + const string& usage) { + auto& cmd = add_command(cli, name, usage); + add_argument(cmd, "shape", params.shape, "Input shape."); + add_option(cmd, "output", params.output, "Output shape."); + add_option(cmd, "samples", params.samples, "Number of samples."); +} + +int run_sample(const sample_params& params) { + // load mesh + auto shape = scene_shape{}; + auto ioerror = ""s; + if (!load_shape(params.shape, shape, ioerror)) print_fatal(ioerror); + + // generate samples + auto samples = sample_shape(shape, params.samples); + + // sample shape + auto sshape = scene_shape{}; + for (auto& sample : samples) { + sshape.points.push_back((int)sshape.points.size()); + sshape.positions.push_back(eval_position(shape, sample.element, sample.uv)); + sshape.radius.push_back(0.001f); + } + + // save mesh + if (!save_shape(params.output, sshape, ioerror, true)) print_fatal(ioerror); + + // done + return 0; +} + struct glview_params { string shape = "shape.ply"; bool addsky = false; @@ -428,6 +532,8 @@ struct app_params { fvconvert_params fvconvert = {}; view_params view = {}; heightfield_params heightfield = {}; + hair_params hair = {}; + sample_params sample = {}; glview_params glview = {}; }; @@ -441,6 +547,8 @@ void add_commands(cli_command& cli, const string& name, app_params& params, cli, "fvconvert", params.fvconvert, "Convert face-varying shapes."); add_command(cli, "view", params.view, "View shapes."); add_command(cli, "heightfield", params.heightfield, "Create an heightfield."); + add_command(cli, "hair", params.hair, "Grow hairs on a shape."); + add_command(cli, "sample", params.sample, "Sample shapepoints on a shape."); add_command(cli, "glview", params.glview, "View shapes with OpenGL."); } @@ -465,6 +573,10 @@ int main(int argc, const char* argv[]) { return run_view(params.view); } else if (params.command == "heightfield") { return run_heightfield(params.heightfield); + } else if (params.command == "hair") { + return run_hair(params.hair); + } else if (params.command == "sample") { + return run_sample(params.sample); } else if (params.command == "glview") { return run_glview(params.glview); } else { diff --git a/libs/yocto/yocto_image.h b/libs/yocto/yocto_image.h index ec4d92558..da20b1529 100644 --- a/libs/yocto/yocto_image.h +++ b/libs/yocto/yocto_image.h @@ -311,6 +311,14 @@ void make_ridgemap(vector& pixels, int width, int height, float scale = 1, const vec4f& noise = {2, 0.5, 8, 1}, const vec4f& color0 = {0, 0, 0, 1}, const vec4f& color1 = {1, 1, 1, 1}); +// Make a random image. +void make_randpoints(vector& pixels, int width, int height, + float scale = 1, const vec4f& color0 = vec4f{0.2f, 0.2f, 0.2f, 1.0f}, + const vec4f& color1 = vec4f{0.5f, 0.5f, 0.5f, 1.0f}); +void make_randlines(vector& pixels, int width, int height, + float scale = 1, const vec4f& color0 = vec4f{0.2f, 0.2f, 0.2f, 1.0f}, + const vec4f& color1 = vec4f{0.5f, 0.5f, 0.5f, 1.0f}); + // Make a sunsky HDR model with sun at sun_angle elevation in [0,pif/2], // turbidity in [1.7,10] with or without sun. The sun can be enabled or // disabled with has_sun. The sun parameters can be slightly modified by diff --git a/libs/yocto/yocto_scene.cpp b/libs/yocto/yocto_scene.cpp index fcbc6cac4..69887674f 100644 --- a/libs/yocto/yocto_scene.cpp +++ b/libs/yocto/yocto_scene.cpp @@ -1484,6 +1484,15 @@ scene_shape make_uvsphere( return shape; } +// Make a sphere. +scene_shape make_uvspherey( + const vec2i& steps, float scale, const vec2f& uvscale) { + auto shape = scene_shape{}; + make_uvspherey(shape.quads, shape.positions, shape.normals, shape.texcoords, + steps, scale, uvscale); + return shape; +} + // Make a sphere with slipped caps. scene_shape make_capped_uvsphere( const vec2i& steps, float scale, const vec2f& uvscale, float height) { @@ -1492,6 +1501,16 @@ scene_shape make_capped_uvsphere( shape.texcoords, steps, scale, uvscale, height); return shape; } + +// Make a sphere with slipped caps. +scene_shape make_capped_uvspherey( + const vec2i& steps, float scale, const vec2f& uvscale, float height) { + auto shape = scene_shape{}; + make_capped_uvspherey(shape.quads, shape.positions, shape.normals, + shape.texcoords, steps, scale, uvscale, height); + return shape; +} + // Make a disk scene_shape make_disk(int steps, float scale, float uvscale) { auto shape = scene_shape{}; @@ -1594,49 +1613,61 @@ scene_fvshape make_fvsphere(int steps, float scale, float uvscale) { } // Predefined meshes -scene_shape make_monkey(float scale) { +scene_shape make_monkey(float scale, int subdivisions) { auto shape = scene_shape{}; - make_monkey(shape.quads, shape.positions, scale); + make_monkey(shape.quads, shape.positions, scale, subdivisions); return shape; } -scene_shape make_quad(float scale) { +scene_shape make_quad(float scale, int subdivisions) { auto shape = scene_shape{}; - make_quad( - shape.quads, shape.positions, shape.normals, shape.texcoords, scale); + make_quad(shape.quads, shape.positions, shape.normals, shape.texcoords, scale, + subdivisions); return shape; } -scene_shape make_quady(float scale) { +scene_shape make_quady(float scale, int subdivisions) { auto shape = scene_shape{}; - make_quady( - shape.quads, shape.positions, shape.normals, shape.texcoords, scale); + make_quady(shape.quads, shape.positions, shape.normals, shape.texcoords, + scale, subdivisions); return shape; } -scene_shape make_cube(float scale) { +scene_shape make_cube(float scale, int subdivisions) { auto shape = scene_shape{}; - make_cube( - shape.quads, shape.positions, shape.normals, shape.texcoords, scale); + make_cube(shape.quads, shape.positions, shape.normals, shape.texcoords, scale, + subdivisions); return shape; } -scene_fvshape make_fvcube(float scale) { +scene_fvshape make_fvcube(float scale, int subdivisions) { auto shape = scene_fvshape{}; make_fvcube(shape.quadspos, shape.quadsnorm, shape.quadstexcoord, - shape.positions, shape.normals, shape.texcoords, scale); + shape.positions, shape.normals, shape.texcoords, scale, subdivisions); return shape; } -scene_shape make_geosphere(float scale) { +scene_shape make_geosphere(float scale, int subdivisions) { auto shape = scene_shape{}; - make_geosphere(shape.triangles, shape.positions, scale); + make_geosphere( + shape.triangles, shape.positions, shape.normals, scale, subdivisions); return shape; } // Make a hair ball around a shape scene_shape make_hair(const scene_shape& base, const vec2i& steps, - const vec2f& len, const vec2f& rad, const vec2f& noise, const vec2f& clump, - const vec2f& rotation, int seed) { + const vec2f& length, const vec2f& radius, const vec2f& noise, + const vec2f& clump, const vec2f& rotation, int seed) { auto shape = scene_shape{}; make_hair(shape.lines, shape.positions, shape.normals, shape.texcoords, shape.radius, base.triangles, base.quads, base.positions, base.normals, - base.texcoords, steps, len, rad, noise, clump, rotation, seed); + base.texcoords, steps, length, radius, noise, clump, rotation, seed); + return shape; +} + +// Grow hairs around a shape +scene_shape make_hair2(const scene_shape& base, const vec2i& steps, + const vec2f& length, const vec2f& radius, float noise, float gravity, + int seed) { + auto shape = scene_shape{}; + make_hair2(shape.lines, shape.positions, shape.normals, shape.texcoords, + shape.radius, base.triangles, base.quads, base.positions, base.normals, + base.texcoords, steps, length, radius, noise, gravity, seed); return shape; } @@ -1656,7 +1687,7 @@ scene_shape make_heightfield(const vec2i& size, const vector& color) { // Convert points to small spheres and lines to small cylinders. This is // intended for making very small primitives for display in interactive -// applications, so the spheres are low res and without texcoords and normals. +// applications, so the spheres are low res. scene_shape points_to_spheres( const vector& vertices, int steps, float scale) { auto shape = scene_shape{}; @@ -1678,6 +1709,13 @@ scene_shape lines_to_cylinders( shape.texcoords, vertices, steps, scale); return shape; } +scene_shape lines_to_cylinders(const vector& lines, + const vector& positions, int steps, float scale) { + auto shape = scene_shape{}; + lines_to_cylinders(shape.quads, shape.positions, shape.normals, + shape.texcoords, lines, positions, steps, scale); + return shape; +} } // namespace yocto diff --git a/libs/yocto/yocto_scene.h b/libs/yocto/yocto_scene.h index cc545c1ee..9eef28795 100644 --- a/libs/yocto/yocto_scene.h +++ b/libs/yocto/yocto_scene.h @@ -342,8 +342,8 @@ vector sample_shape_cdf(const scene_shape& shape); void sample_shape_cdf(vector& cdf, const scene_shape& shape); shape_point sample_shape(const scene_shape& shape, const vector& cdf, float rn, const vec2f& ruv); -vector sample_shape(const scene_shape& shape, - const vector& cdf, int num_samples, uint64_t seed = 98729387); +vector sample_shape( + const scene_shape& shape, int num_samples, uint64_t seed = 98729387); // Conversions scene_shape quads_to_triangles(const scene_shape& shape); @@ -497,9 +497,13 @@ scene_shape make_sphere(int steps = 32, float scale = 1, float uvscale = 1); // Make a sphere. scene_shape make_uvsphere(const vec2i& steps = {32, 32}, float scale = 1, const vec2f& uvscale = {1, 1}); +scene_shape make_uvspherey(const vec2i& steps = {32, 32}, float scale = 1, + const vec2f& uvscale = {1, 1}); // Make a sphere with slipped caps. scene_shape make_capped_uvsphere(const vec2i& steps = {32, 32}, float scale = 1, const vec2f& uvscale = {1, 1}, float height = 0.3); +scene_shape make_capped_uvspherey(const vec2i& steps = {32, 32}, + float scale = 1, const vec2f& uvscale = {1, 1}, float height = 0.3); // Make a disk scene_shape make_disk(int steps = 32, float scale = 1, float uvscale = 1); // Make a bulged disk @@ -538,12 +542,12 @@ scene_shape make_random_points(int num = 65536, const vec3f& size = {1, 1, 1}, float uvscale = 1, float radius = 0.001f, uint64_t seed = 17); // Predefined meshes -scene_shape make_monkey(float scale = 1); -scene_shape make_quad(float scale = 1); -scene_shape make_quady(float scale = 1); -scene_shape make_cube(float scale = 1); -scene_fvshape make_fvcube(float scale = 1); -scene_shape make_geosphere(float scale = 1); +scene_shape make_monkey(float scale = 1, int subdivisions = 0); +scene_shape make_quad(float scale = 1, int subdivisions = 0); +scene_shape make_quady(float scale = 1, int subdivisions = 0); +scene_shape make_cube(float scale = 1, int subdivisions = 0); +scene_fvshape make_fvcube(float scale = 1, int subdivisions = 0); +scene_shape make_geosphere(float scale = 1, int subdivisions = 0); // Make a hair ball around a shape. // length: minimum and maximum length @@ -552,19 +556,27 @@ scene_shape make_geosphere(float scale = 1); // clump: clump added to hair (strength/number) // rotation: rotation added to hair (angle/strength) scene_shape make_hair(const scene_shape& shape, const vec2i& steps = {8, 65536}, - const vec2f& length = {0.1f, 0.1f}, const vec2f& rad = {0.001f, 0.001f}, + const vec2f& length = {0.1f, 0.1f}, const vec2f& radius = {0.001f, 0.001f}, const vec2f& noise = {0, 10}, const vec2f& clump = {0, 128}, const vec2f& rotation = {0, 0}, int seed = 7); +// Grow hairs around a shape +scene_shape make_hair2(const scene_shape& shape, + const vec2i& steps = {8, 65536}, const vec2f& length = {0.1f, 0.1f}, + const vec2f& radius = {0.001f, 0.001f}, float noise = 0, + float gravity = 0.001f, int seed = 7); + // Convert points to small spheres and lines to small cylinders. This is // intended for making very small primitives for display in interactive -// applications, so the spheres are low res and without texcoords and normals. +// applications, so the spheres are low res. scene_shape points_to_spheres( const vector& vertices, int steps = 2, float scale = 0.01f); scene_shape polyline_to_cylinders( const vector& vertices, int steps = 4, float scale = 0.01f); scene_shape lines_to_cylinders( const vector& vertices, int steps = 4, float scale = 0.01f); +scene_shape lines_to_cylinders(const vector& lines, + const vector& positions, int steps = 4, float scale = 0.01f); // Make a heightfield mesh. scene_shape make_heightfield(const vec2i& size, const vector& height); diff --git a/libs/yocto/yocto_sceneio.cpp b/libs/yocto/yocto_sceneio.cpp index 263f599d7..866705ee0 100644 --- a/libs/yocto/yocto_sceneio.cpp +++ b/libs/yocto/yocto_sceneio.cpp @@ -1323,6 +1323,10 @@ bool make_shape_preset(scene_shape& shape, const string& type, string& error) { set_quads(make_rounded_box()); } else if (type == "default-sphere") { set_quads(make_sphere()); + } else if (type == "default-matcube") { + set_quads(make_rounded_box()); + } else if (type == "default-matsphere") { + set_quads(make_uvspherey()); } else if (type == "default-disk") { set_quads(make_disk()); } else if (type == "default-disk-bulged") { @@ -1333,6 +1337,10 @@ bool make_shape_preset(scene_shape& shape, const string& type, string& error) { set_quads(make_uvsphere()); } else if (type == "default-uvsphere-flipcap") { set_quads(make_capped_uvsphere()); + } else if (type == "default-uvspherey") { + set_quads(make_uvspherey()); + } else if (type == "default-uvspherey-flipcap") { + set_quads(make_capped_uvspherey()); } else if (type == "default-uvdisk") { set_quads(make_uvdisk()); } else if (type == "default-uvcylinder") { @@ -1364,7 +1372,7 @@ bool make_shape_preset(scene_shape& shape, const string& type, string& error) { set_quads(make_sphere(128)); } else if (type == "test-cube") { set_quads(make_rounded_box( - {32, 32, 32}, {0.075f, 0.075f, 0.075f}, {1, 1, 1}, 0.3 * 0.075f)); + {32, 32, 32}, {0.075f, 0.075f, 0.075f}, {1, 1, 1}, 0.3f * 0.075f)); for (auto& p : shape.positions) p += {0, 0.075f, 0}; } else if (type == "test-uvsphere") { set_quads(make_uvsphere({32, 32}, 0.075f)); @@ -1372,12 +1380,28 @@ bool make_shape_preset(scene_shape& shape, const string& type, string& error) { } else if (type == "test-uvsphere-flipcap") { set_quads(make_capped_uvsphere({32, 32}, 0.075f, {1, 1}, 0.3f * 0.075f)); for (auto& p : shape.positions) p += {0, 0.075f, 0}; + } else if (type == "test-uvspherey") { + set_quads(make_uvspherey({32, 32}, 0.075f)); + for (auto& p : shape.positions) p += {0, 0.075f, 0}; + } else if (type == "test-uvspherey-flipcap") { + set_quads(make_capped_uvspherey({32, 32}, 0.075f, {1, 1}, 0.3f * 0.075f)); + for (auto& p : shape.positions) p += {0, 0.075f, 0}; } else if (type == "test-sphere") { set_quads(make_sphere(32, 0.075f, 1)); for (auto& p : shape.positions) p += {0, 0.075f, 0}; + } else if (type == "test-matcube") { + set_quads(make_rounded_box( + {32, 32, 32}, {0.075f, 0.075f, 0.075f}, {1, 1, 1}, 0.3f * 0.075f)); + for (auto& p : shape.positions) p += {0, 0.075f, 0}; + } else if (type == "test-matsphere") { + set_quads(make_uvspherey({32, 32}, 0.075f, {2, 1})); + for (auto& p : shape.positions) p += {0, 0.075f, 0}; } else if (type == "test-sphere-displaced") { set_quads(make_sphere(128, 0.075f, 1)); for (auto& p : shape.positions) p += {0, 0.075f, 0}; + } else if (type == "test-smallsphere") { + set_quads(make_sphere(32, 0.015f, 1)); + for (auto& p : shape.positions) p += {0, 0.015f, 0}; } else if (type == "test-disk") { set_quads(make_disk(32, 0.075f, 1)); for (auto& p : shape.positions) p += {0, 0.075f, 0}; @@ -1387,6 +1411,8 @@ bool make_shape_preset(scene_shape& shape, const string& type, string& error) { for (auto& p : shape.positions) p += {0, 0.075f, 0}; } else if (type == "test-floor") { set_quads(make_floor({1, 1}, {2, 2}, {20, 20})); + } else if (type == "test-smallfloor") { + set_quads(make_floor({1, 1}, {0.5f, 0.5f}, {1, 1})); } else if (type == "test-quad") { set_quads(make_rect({1, 1}, {0.075f, 0.075f}, {1, 1})); } else if (type == "test-quady") { @@ -1398,6 +1424,16 @@ bool make_shape_preset(scene_shape& shape, const string& type, string& error) { } else if (type == "test-matball") { set_quads(make_sphere(32, 0.075f)); for (auto& p : shape.positions) p += {0, 0.075f, 0}; + } else if (type == "test-geosphere") { + set_triangles(make_geosphere(0.075f, 3)); + for (auto& p : shape.positions) p += {0, 0.075f, 0}; + } else if (type == "test-geosphere-flat") { + set_triangles(make_geosphere(0.075f, 3)); + for (auto& p : shape.positions) p += {0, 0.075f, 0}; + shape.normals = {}; + } else if (type == "test-geosphere-subdivided") { + set_triangles(make_geosphere(0.075f, 6)); + for (auto& p : shape.positions) p += {0, 0.075f, 0}; } else if (type == "test-hairball1") { auto base = make_sphere(32, 0.075f * 0.8f, 1); for (auto& p : base.positions) p += {0, 0.075f, 0}; @@ -1442,7 +1478,8 @@ bool make_shape_preset(scene_shape& shape, const string& type, string& error) { } else if (type == "test-points") { set_points(make_points(4096)); } else if (type == "test-points-random") { - set_points(make_random_points(4096, {0.2f, 0.2f, 0.2f})); + set_points(make_random_points(4096, {0.075f, 0.075f, 0.075f})); + for (auto& p : shape.positions) p += {0, 0.075f, 0}; } else if (type == "test-particles") { set_points(make_points(4096)); } else if (type == "test-cloth") { @@ -1495,6 +1532,10 @@ bool make_fvshape_preset( set_quads(make_rounded_box()); } else if (type == "default-sphere") { set_quads(make_sphere()); + } else if (type == "default-matcube") { + set_quads(make_rounded_box()); + } else if (type == "default-matsphere") { + set_quads(make_uvspherey()); } else if (type == "default-disk") { set_quads(make_disk()); } else if (type == "default-disk-bulged") { @@ -1505,6 +1546,10 @@ bool make_fvshape_preset( set_quads(make_uvsphere()); } else if (type == "default-uvsphere-flipcap") { set_quads(make_capped_uvsphere()); + } else if (type == "default-uvspherey") { + set_quads(make_uvspherey()); + } else if (type == "default-uvspherey-flipcap") { + set_quads(make_capped_uvspherey()); } else if (type == "default-uvdisk") { set_quads(make_uvdisk()); } else if (type == "default-uvcylinder") { @@ -1538,18 +1583,31 @@ bool make_fvshape_preset( set_quads(make_rounded_box( {32, 32, 32}, {0.075f, 0.075f, 0.075f}, {1, 1, 1}, 0.3f * 0.075f)); for (auto& p : shape.positions) p += {0, 0.075f, 0}; + } else if (type == "test-matsphere") { + set_quads(make_uvspherey({32, 32}, 0.075f, {2, 1})); + for (auto& p : shape.positions) p += {0, 0.075f, 0}; } else if (type == "test-uvsphere") { set_quads(make_uvsphere({32, 32}, 0.075f)); for (auto& p : shape.positions) p += {0, 0.075f, 0}; } else if (type == "test-uvsphere-flipcap") { set_quads(make_capped_uvsphere({32, 32}, 0.075f, {1, 1}, 0.3f * 0.075f)); for (auto& p : shape.positions) p += {0, 0.075f, 0}; + } else if (type == "test-uvspherey") { + set_quads(make_uvspherey({32, 32}, 0.075f)); + for (auto& p : shape.positions) p += {0, 0.075f, 0}; + } else if (type == "test-uvspherey-flipcap") { + set_quads(make_capped_uvspherey({32, 32}, 0.075f, {1, 1}, 0.3f * 0.075f)); + for (auto& p : shape.positions) p += {0, 0.075f, 0}; } else if (type == "test-sphere") { set_quads(make_sphere(32, 0.075f, 1)); for (auto& p : shape.positions) p += {0, 0.075f, 0}; } else if (type == "test-sphere-displaced") { set_quads(make_sphere(128, 0.075f, 1)); for (auto& p : shape.positions) p += {0, 0.075f, 0}; + } else if (type == "test-matcube") { + set_quads(make_rounded_box( + {32, 32, 32}, {0.075f, 0.075f, 0.075f}, {1, 1, 1}, 0.3f * 0.075f)); + for (auto& p : shape.positions) p += {0, 0.075f, 0}; } else if (type == "test-disk") { set_quads(make_disk(32, 0.075f, 1)); for (auto& p : shape.positions) p += {0, 0.075f, 0}; @@ -1559,6 +1617,8 @@ bool make_fvshape_preset( for (auto& p : shape.positions) p += {0, 0.075f, 0}; } else if (type == "test-floor") { set_quads(make_floor({1, 1}, {2, 2}, {20, 20})); + } else if (type == "test-smallfloor") { + set_quads(make_floor({1, 1}, {0.5f, 0.5f}, {1, 1})); } else if (type == "test-quad") { set_quads(make_rect({1, 1}, {0.075f, 0.075f}, {1, 1})); } else if (type == "test-quady") { diff --git a/libs/yocto/yocto_shape.cpp b/libs/yocto/yocto_shape.cpp index 8b9aab193..978461e43 100644 --- a/libs/yocto/yocto_shape.cpp +++ b/libs/yocto/yocto_shape.cpp @@ -2220,6 +2220,27 @@ void make_capped_uvsphere(vector& quads, vector& positions, } } +// Generate a uvsphere +void make_uvspherey(vector& quads, vector& positions, + vector& normals, vector& texcoords, const vec2i& steps, + float scale, const vec2f& uvscale) { + make_uvsphere(quads, positions, normals, texcoords, steps, scale, uvscale); + for (auto& p : positions) std::swap(p.y, p.z); + for (auto& n : normals) std::swap(n.y, n.z); + for (auto& t : texcoords) t.y = 1 - t.y; + for (auto& q : quads) std::swap(q.y, q.w); +} + +void make_capped_uvspherey(vector& quads, vector& positions, + vector& normals, vector& texcoords, const vec2i& steps, + float scale, const vec2f& uvscale, float cap) { + make_capped_uvsphere( + quads, positions, normals, texcoords, steps, scale, uvscale, cap); + for (auto& p : positions) std::swap(p.y, p.z); + for (auto& n : normals) std::swap(n.y, n.z); + for (auto& q : quads) std::swap(q.y, q.w); +} + // Generate a disk void make_disk(vector& quads, vector& positions, vector& normals, vector& texcoords, int steps, float scale, @@ -2285,6 +2306,7 @@ void make_uvcylinder(vector& quads, vector& positions, qnormals[i] = {cos(phi), sin(phi), 0}; qtexcoords[i] = uv * vec2f{uvscale.x, uvscale.y}; } + for (auto& q : qquads) std::swap(q.y, q.w); merge_quads(quads, positions, normals, texcoords, qquads, qpositions, qnormals, qtexcoords); // top @@ -2348,39 +2370,36 @@ void make_lines(vector& lines, vector& positions, vector& normals, vector& texcoords, vector& radius, const vec2i& steps, const vec2f& size, const vec2f& uvscale, const vec2f& rad) { - auto nverts = (steps.x + 1) * steps.y; - auto nlines = steps.x * steps.y; - auto vid = [steps](int i, int j) { return j * (steps.x + 1) + i; }; - auto fid = [steps](int i, int j) { return j * steps.x + i; }; - - positions.resize(nverts); - normals.resize(nverts); - texcoords.resize(nverts); - radius.resize(nverts); + positions.resize((steps.x + 1) * steps.y); + normals.resize((steps.x + 1) * steps.y); + texcoords.resize((steps.x + 1) * steps.y); + radius.resize((steps.x + 1) * steps.y); if (steps.y > 1) { for (auto j = 0; j < steps.y; j++) { for (auto i = 0; i <= steps.x; i++) { - auto uv = vec2f{ - i / (float)steps.x, j / (float)(steps.y > 1 ? steps.y - 1 : 1)}; - positions[vid(i, j)] = { + auto uv = vec2f{i / (float)steps.x, j / (float)(steps.y - 1)}; + positions[j * (steps.x + 1) + i] = { (uv.x - 0.5f) * size.x, (uv.y - 0.5f) * size.y, 0}; - normals[vid(i, j)] = {1, 0, 0}; - texcoords[vid(i, j)] = uv * uvscale; + normals[j * (steps.x + 1) + i] = {1, 0, 0}; + texcoords[j * (steps.x + 1) + i] = uv * uvscale; + radius[j * (steps.x + 1) + i] = lerp(rad.x, rad.y, uv.x); } } } else { for (auto i = 0; i <= steps.x; i++) { - auto uv = vec2f{i / (float)steps.x, 0}; - positions[vid(i, 0)] = {(uv.x - 0.5f) * size.x, 0, 0}; - normals[vid(i, 0)] = {1, 0, 0}; - texcoords[vid(i, 0)] = uv * uvscale; + auto uv = vec2f{i / (float)steps.x, 0}; + positions[i] = {(uv.x - 0.5f) * size.x, 0, 0}; + normals[i] = {1, 0, 0}; + texcoords[i] = uv * uvscale; + radius[i] = lerp(rad.x, rad.y, uv.x); } } - lines.resize(nlines); + lines.resize(steps.x * steps.y); for (int j = 0; j < steps.y; j++) { for (int i = 0; i < steps.x; i++) { - lines[fid(i, j)] = {vid(i, j), vid(i + 1, j)}; + lines[j * steps.x + i] = { + j * (steps.x + 1) + i, j * (steps.x + 1) + i + 1}; } } } @@ -2419,9 +2438,8 @@ void make_random_points(vector& points, vector& positions, make_points(points, positions, normals, texcoords, radius, num, uvscale, point_radius); auto rng = make_rng(seed); - for (auto& position : positions) { - position = (rand3f(rng) - vec3f{0.5f, 0.5f, 0.5f}) * size; - } + for (auto& position : positions) position = (2 * rand3f(rng) - 1) * size; + for (auto& texcoord : texcoords) texcoord = rand2f(rng); } // Make a bezier circle. Returns bezier, pos. @@ -2889,16 +2907,23 @@ vector suzanne_quads = vector{{46, 0, 2, 44}, {3, 1, 47, 45}, {500, 498, 496, 496}}; // Predefined meshes -void make_monkey(vector& quads, vector& positions, float scale) { - quads = suzanne_quads; - positions = suzanne_positions; +void make_monkey(vector& quads, vector& positions, float scale, + int subdivisions) { + if (subdivisions == 0) { + quads = suzanne_quads; + positions = suzanne_positions; + } else { + std::tie(quads, positions) = subdivide_quads( + suzanne_quads, suzanne_positions, subdivisions); + } if (scale != 1) { for (auto& p : positions) p *= scale; } } void make_quad(vector& quads, vector& positions, - vector& normals, vector& texcoords, float scale) { + vector& normals, vector& texcoords, float scale, + int subdivisions) { static const auto quad_positions = vector{ {-1, -1, 0}, {+1, -1, 0}, {+1, +1, 0}, {-1, +1, 0}}; static const auto quad_normals = vector{ @@ -2906,17 +2931,27 @@ void make_quad(vector& quads, vector& positions, static const auto quad_texcoords = vector{ {0, 1}, {1, 1}, {1, 0}, {0, 0}}; static const auto quad_quads = vector{{0, 1, 2, 3}}; - quads = quad_quads; - positions = quad_positions; - normals = quad_normals; - texcoords = quad_texcoords; + if (subdivisions == 0) { + quads = quad_quads; + positions = quad_positions; + normals = quad_normals; + texcoords = quad_texcoords; + } else { + std::tie(quads, positions) = subdivide_quads( + quad_quads, quad_positions, subdivisions); + std::tie(quads, normals) = subdivide_quads( + quad_quads, quad_normals, subdivisions); + std::tie(quads, texcoords) = subdivide_quads( + quad_quads, quad_texcoords, subdivisions); + } if (scale != 1) { for (auto& p : positions) p *= scale; } } void make_quady(vector& quads, vector& positions, - vector& normals, vector& texcoords, float scale) { + vector& normals, vector& texcoords, float scale, + int subdivisions) { static const auto quady_positions = vector{ {-1, 0, -1}, {-1, 0, +1}, {+1, 0, +1}, {+1, 0, -1}}; static const auto quady_normals = vector{ @@ -2924,17 +2959,27 @@ void make_quady(vector& quads, vector& positions, static const auto quady_texcoords = vector{ {0, 0}, {1, 0}, {1, 1}, {0, 1}}; static const auto quady_quads = vector{{0, 1, 2, 3}}; - quads = quady_quads; - positions = quady_positions; - normals = quady_normals; - texcoords = quady_texcoords; + if (subdivisions == 0) { + quads = quady_quads; + positions = quady_positions; + normals = quady_normals; + texcoords = quady_texcoords; + } else { + std::tie(quads, positions) = subdivide_quads( + quady_quads, quady_positions, subdivisions); + std::tie(quads, normals) = subdivide_quads( + quady_quads, quady_normals, subdivisions); + std::tie(quads, texcoords) = subdivide_quads( + quady_quads, quady_texcoords, subdivisions); + } if (scale != 1) { for (auto& p : positions) p *= scale; } } void make_cube(vector& quads, vector& positions, - vector& normals, vector& texcoords, float scale) { + vector& normals, vector& texcoords, float scale, + int subdivisions) { static const auto cube_positions = vector{{-1, -1, +1}, {+1, -1, +1}, {+1, +1, +1}, {-1, +1, +1}, {+1, -1, -1}, {-1, -1, -1}, {-1, +1, -1}, {+1, +1, -1}, {+1, -1, +1}, {+1, -1, -1}, {+1, +1, -1}, {+1, +1, +1}, @@ -2952,10 +2997,19 @@ void make_cube(vector& quads, vector& positions, {1, 1}, {1, 0}, {0, 0}}; static const auto cube_quads = vector{{0, 1, 2, 3}, {4, 5, 6, 7}, {8, 9, 10, 11}, {12, 13, 14, 15}, {16, 17, 18, 19}, {20, 21, 22, 23}}; - quads = cube_quads; - positions = cube_positions; - normals = cube_normals; - texcoords = cube_texcoords; + if (subdivisions == 0) { + quads = cube_quads; + positions = cube_positions; + normals = cube_normals; + texcoords = cube_texcoords; + } else { + std::tie(quads, positions) = subdivide_quads( + cube_quads, cube_positions, subdivisions); + std::tie(quads, normals) = subdivide_quads( + cube_quads, cube_normals, subdivisions); + std::tie(quads, texcoords) = subdivide_quads( + cube_quads, cube_texcoords, subdivisions); + } if (scale != 1) { for (auto& p : positions) p *= scale; } @@ -2963,7 +3017,8 @@ void make_cube(vector& quads, vector& positions, void make_fvcube(vector& quadspos, vector& quadsnorm, vector& quadstexcoord, vector& positions, - vector& normals, vector& texcoords, float scale) { + vector& normals, vector& texcoords, float scale, + int subdivisions) { static const auto fvcube_positions = vector{{-1, -1, +1}, {+1, -1, +1}, {+1, +1, +1}, {-1, +1, +1}, {+1, -1, -1}, {-1, -1, -1}, {-1, +1, -1}, {+1, +1, -1}}; @@ -2983,19 +3038,28 @@ void make_fvcube(vector& quadspos, vector& quadsnorm, static const auto fvcube_quadstexcoord = vector{{0, 1, 2, 3}, {4, 5, 6, 7}, {8, 9, 10, 11}, {12, 13, 14, 15}, {16, 17, 18, 19}, {20, 21, 22, 23}}; - quadspos = fvcube_quadspos; - quadsnorm = fvcube_quadsnorm; - quadstexcoord = fvcube_quadstexcoord; - positions = fvcube_positions; - normals = fvcube_normals; - texcoords = fvcube_texcoords; + if (subdivisions == 0) { + quadspos = fvcube_quadspos; + quadsnorm = fvcube_quadsnorm; + quadstexcoord = fvcube_quadstexcoord; + positions = fvcube_positions; + normals = fvcube_normals; + texcoords = fvcube_texcoords; + } else { + std::tie(quadspos, positions) = subdivide_quads( + fvcube_quadspos, fvcube_positions, subdivisions); + std::tie(quadsnorm, normals) = subdivide_quads( + fvcube_quadsnorm, fvcube_normals, subdivisions); + std::tie(quadstexcoord, texcoords) = subdivide_quads( + fvcube_quadstexcoord, fvcube_texcoords, subdivisions); + } if (scale != 1) { for (auto& p : positions) p *= scale; } } -void make_geosphere( - vector& triangles, vector& positions, float scale) { +void make_geosphere(vector& triangles, vector& positions, + vector& normals, float scale, int subdivisions) { // https://stackoverflow.com/questions/17705621/algorithm-for-a-geodesic-sphere const float X = 0.525731112119133606f; const float Z = 0.850650808352039932f; @@ -3006,8 +3070,16 @@ void make_geosphere( {9, 4, 5}, {4, 8, 5}, {4, 1, 8}, {8, 1, 10}, {8, 10, 3}, {5, 8, 3}, {5, 3, 2}, {2, 3, 7}, {7, 3, 10}, {7, 10, 6}, {7, 6, 11}, {11, 6, 0}, {0, 6, 1}, {6, 10, 1}, {9, 11, 0}, {9, 2, 11}, {9, 5, 2}, {7, 11, 2}}; - triangles = geosphere_triangles; - positions = geosphere_positions; + if (subdivisions == 0) { + triangles = geosphere_triangles; + positions = geosphere_positions; + normals = geosphere_positions; + } else { + std::tie(triangles, positions) = subdivide_triangles( + geosphere_triangles, geosphere_positions, subdivisions); + for (auto& position : positions) position = normalize(position); + normals = positions; + } if (scale != 1) { for (auto& p : positions) p *= scale; } @@ -3085,6 +3157,45 @@ void make_hair(vector& lines, vector& positions, } } +// Grow hairs around a shape +void make_hair2(vector& lines, vector& positions, + vector& normals, vector& texcoords, vector& radius, + const vector& striangles, const vector& squads, + const vector& spos, const vector& snorm, + const vector& stexcoord, const vec2i& steps, const vec2f& len, + const vec2f& rad, float noise, float gravity, int seed) { + auto alltriangles = striangles; + auto quads_triangles = quads_to_triangles(squads); + alltriangles.insert( + alltriangles.end(), quads_triangles.begin(), quads_triangles.end()); + auto bpositions = vector{}; + auto bnormals = vector{}; + auto btexcoord = vector{}; + sample_triangles(bpositions, bnormals, btexcoord, alltriangles, spos, snorm, + stexcoord, steps.y, seed); + + make_lines( + lines, positions, normals, texcoords, radius, steps, {1, 1}, {1, 1}, rad); + auto rng = make_rng(seed); + for (auto idx = 0; idx < steps.y; idx++) { + auto offset = idx * (steps.x + 1); + auto position = bpositions[idx]; + auto direction = bnormals[idx]; + auto length = rand1f(rng) * (len.y - len.x) + len.x; + positions[offset] = position; + for (auto iidx = 1; iidx <= steps.x; iidx++) { + positions[offset + iidx] = position; + positions[offset + iidx] += direction * length / steps.x; + positions[offset + iidx] += (2 * rand3f(rng) - 1) * noise; + positions[offset + iidx] += vec3f{0, -gravity, 0}; + direction = normalize(positions[offset + iidx] - position); + position = positions[offset + iidx]; + } + } + + normals = lines_tangents(lines, positions); +} + // Thickens a shape by copy9ing the shape content, rescaling it and flipping // its normals. Note that this is very much not robust and only useful for // trivial cases. @@ -3194,4 +3305,29 @@ void lines_to_cylinders(vector& quads, vector& positions, } } +void lines_to_cylinders(vector& quads, vector& positions, + vector& normals, vector& texcoords, + const vector& lines, const vector& vertices, int steps, + float scale) { + auto cylinder_quads = vector{}; + auto cylinder_positions = vector{}; + auto cylinder_normals = vector{}; + auto cylinder_texcoords = vector{}; + make_uvcylinder(cylinder_quads, cylinder_positions, cylinder_normals, + cylinder_texcoords, {steps, 1, 1}, {scale, 1}, {1, 1, 1}); + for (auto& line : lines) { + auto frame = frame_fromz((vertices[line.x] + vertices[line.y]) / 2, + vertices[line.x] - vertices[line.y]); + auto length = distance(vertices[line.x], vertices[line.y]); + auto transformed_positions = cylinder_positions; + auto transformed_normals = cylinder_normals; + for (auto& position : transformed_positions) + position = transform_point(frame, position * vec3f{1, 1, length / 2}); + for (auto& normal : transformed_normals) + normal = transform_direction(frame, normal); + merge_quads(quads, positions, normals, texcoords, cylinder_quads, + transformed_positions, cylinder_normals, cylinder_texcoords); + } +} + } // namespace yocto diff --git a/libs/yocto/yocto_shape.h b/libs/yocto/yocto_shape.h index c7a10064d..bd560187d 100644 --- a/libs/yocto/yocto_shape.h +++ b/libs/yocto/yocto_shape.h @@ -552,6 +552,12 @@ void make_uvsphere(vector& quads, vector& positions, void make_capped_uvsphere(vector& quads, vector& positions, vector& normals, vector& texcoords, const vec2i& steps, float scale, const vec2f& uvscale, float cap); +void make_uvspherey(vector& quads, vector& positions, + vector& normals, vector& texcoords, const vec2i& steps, + float scale, const vec2f& uvscale); +void make_capped_uvspherey(vector& quads, vector& positions, + vector& normals, vector& texcoords, const vec2i& steps, + float scale, const vec2f& uvscale, float cap); // Generate a disk void make_disk(vector& quads, vector& positions, vector& normals, vector& texcoords, int steps, float scale, @@ -614,18 +620,23 @@ void make_fvsphere(vector& quadspos, vector& quadsnorm, float uvscale); // Predefined meshes -void make_monkey(vector& quads, vector& positions, float scale); +void make_monkey(vector& quads, vector& positions, float scale, + int subdivisions); void make_quad(vector& quads, vector& positions, - vector& normals, vector& texcoords, float scale); + vector& normals, vector& texcoords, float scale, + int subdivisions); void make_quady(vector& quads, vector& positions, - vector& normals, vector& texcoords, float scale); + vector& normals, vector& texcoords, float scale, + int subdivisions); void make_cube(vector& quads, vector& positions, - vector& normals, vector& texcoords, float scale); + vector& normals, vector& texcoords, float scale, + int subdivisions); void make_fvcube(vector& quadspos, vector& quadsnorm, vector& quadstexcoord, vector& positions, - vector& normals, vector& texcoords, float scale); -void make_geosphere( - vector& triangles, vector& positions, float scale); + vector& normals, vector& texcoords, float scale, + int subdivisions); +void make_geosphere(vector& triangles, vector& positions, + vector& normals, float scale, int subdivisions); // Convert points to small spheres and lines to small cylinders. This is // intended for making very small primitives for display in interactive @@ -639,6 +650,10 @@ void polyline_to_cylinders(vector& quads, vector& positions, void lines_to_cylinders(vector& quads, vector& positions, vector& normals, vector& texcoords, const vector& vertices, int steps = 4, float scale = 0.01f); +void lines_to_cylinders(vector& quads, vector& positions, + vector& normals, vector& texcoords, + const vector& lines, const vector& vertices, int steps = 4, + float scale = 0.01f); // Make a hair ball around a shape void make_hair(vector& lines, vector& positions, @@ -649,6 +664,14 @@ void make_hair(vector& lines, vector& positions, const vec2f& rad, const vec2f& noise, const vec2f& clump, const vec2f& rotation, int seed); +// Grow hairs around a shape +void make_hair2(vector& lines, vector& positions, + vector& normals, vector& texcoords, vector& radius, + const vector& striangles, const vector& squads, + const vector& spos, const vector& snorm, + const vector& stexcoord, const vec2i& steps, const vec2f& len, + const vec2f& rad, float noise, float gravity, int seed); + // Thickens a shape by copy9ing the shape content, rescaling it and flipping // its normals. Note that this is very much not robust and only useful for // trivial cases. diff --git a/tests/shapes1/shapes/matsphere.ply b/tests/shapes1/shapes/matsphere.ply new file mode 100644 index 0000000000000000000000000000000000000000..e6f5ba00e967eeb80a2f689d6668d9fb30e3e56f GIT binary patch literal 52592 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 Gv5|i#Z1ZdY literal 0 HcmV?d00001 diff --git a/tests/shapes1/shapes/uvcylinder.ply b/tests/shapes1/shapes/uvcylinder.ply index ff6eb260c7871021274127c16274762466e14fca..e2708958597bd7dfbc711f638dbeee56db626362 100644 GIT binary patch delta 17555 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 XQaG?91N5<=n|I)47u+y!{%QXQ?|MG@