diff --git a/README.md b/README.md index 992efb7..e374c52 100644 --- a/README.md +++ b/README.md @@ -173,7 +173,14 @@ damit die Raw-URL sie ausliefert. ## Features -- **Basis-Formen:** Cube oder Ico-Sphere, einstellbare Subsurf-Stufen. +- **Basis-Formen:** Wuerfel, **Quader** oder Ico-Sphere, einstellbare Subsurf-Stufen. + Der Quader hat frei einstellbare `Quader-Proportionen` (X/Y/Z) plus + `Proportions-Streuung` — damit entstehen flache **Platten** und langgestreckte + **Riegel** statt nur runder Brocken. Gemessen bei Proportion (2.0, 1.5, 0.35): + 3.43 / 2.56 / 0.79 m — Z also 23 % von X. + Die Proportionen werden auf die MESH-DATEN angewendet, nicht auf das Objekt: + der Displace nutzt globale Texturkoordinaten, eine Objekt-Skalierung wuerde + das Rauschen mitverzerren statt die Form zu aendern. - **Noise/Displace:** Voronoi / Clouds / Distorted Noise, Scale + Staerke, Seed. Seed-Variation ueber einen moderaten Welt-Offset (±25) vor dem Displace-Apply – Blenders prozedurale Texturen haben keinen echten Seed-Parameter. diff --git a/blender_manifest.toml b/blender_manifest.toml index 81c5c10..b37d3fb 100644 --- a/blender_manifest.toml +++ b/blender_manifest.toml @@ -1,7 +1,7 @@ schema_version = "1.0.0" id = "stylized_rock_generator" -version = "2.9.0" +version = "2.10.0" name = "Stylized Rock Generator" tagline = "Batch-Generator fuer stylized Rocks mit UE5-Export" maintainer = "D4rkst3r" diff --git a/dist/index.json b/dist/index.json index a9eee63..732dfe3 100644 --- a/dist/index.json +++ b/dist/index.json @@ -7,7 +7,7 @@ "id": "stylized_rock_generator", "name": "Stylized Rock Generator", "tagline": "Batch-Generator fuer stylized Rocks mit UE5-Export", - "version": "2.9.0", + "version": "2.10.0", "type": "add-on", "maintainer": "D4rkst3r", "license": [ @@ -23,9 +23,9 @@ "Mesh", "Modeling" ], - "archive_url": "./stylized_rock_generator-2.9.0.zip", - "archive_size": 19688, - "archive_hash": "sha256:78452434a3f175ae1a415b4d4b2abdb5cb803415499c114c0452fb0a4524959e" + "archive_url": "./stylized_rock_generator-2.10.0.zip", + "archive_size": 20241, + "archive_hash": "sha256:d092ae0a1cfefed9bdd4fbe1ffcd8160f4fffb00033cec346bbde3d14c755254" }, { "schema_version": "1.0.0", diff --git a/dist/stylized_rock_generator-2.10.0.zip b/dist/stylized_rock_generator-2.10.0.zip new file mode 100644 index 0000000..6debf08 Binary files /dev/null and b/dist/stylized_rock_generator-2.10.0.zip differ diff --git a/dist/stylized_rock_generator-2.9.0.zip b/dist/stylized_rock_generator-2.9.0.zip deleted file mode 100644 index 732217f..0000000 Binary files a/dist/stylized_rock_generator-2.9.0.zip and /dev/null differ diff --git a/stylized_rock_generator.py b/stylized_rock_generator.py index 2cedf4c..bf53df8 100644 --- a/stylized_rock_generator.py +++ b/stylized_rock_generator.py @@ -1,7 +1,7 @@ bl_info = { "name": "Stylized Rock Generator", "author": "D4rkst3r", - "version": (2, 9, 0), + "version": (2, 10, 0), "blender": (4, 2, 0), "location": "View3D > Sidebar > Rock Gen", "description": ( @@ -50,7 +50,7 @@ CLASS_SIZES = {'S': 0.45, 'M': 1.0, 'L': 1.9, 'XL': 3.2} # Property-Keys, die von Presets gespeichert/geladen werden. PRESET_KEYS = ( - "base_shape", "subsurf_levels", "texture_type", "noise_scale", + "base_shape", "box_proportions", "box_variation", "subsurf_levels", "texture_type", "noise_scale", "displace_strength", "seed", "decimate_ratio_1", "decimate_ratio_2", "dissolve_angle", "generate_uv0", "generate_lightmap_uv", "randomize_transform", "scale_min", "scale_max", @@ -303,8 +303,24 @@ def _make_lightmap_uv(context, obj): def _build_rock_mesh(context, settings, index, location, rng, name=None): """Baut das reine Rock-Mesh (ohne Transform/Material). Kann RockGenError werfen.""" # 1. Basis-Mesh - if settings.base_shape == 'CUBE': + if settings.base_shape in ('CUBE', 'BOX'): bpy.ops.mesh.primitive_cube_add(size=2, location=location) + if settings.base_shape == 'BOX': + # Proportionen auf die MESH-DATEN anwenden, nicht auf das Objekt: + # der Displace nutzt globale Texturkoordinaten, eine Objekt-Skalierung + # wuerde das Rauschen mitverzerren statt die Form zu aendern. + px, py, pz = settings.box_proportions + v = settings.box_variation + if v > 0.0: + px *= 1.0 + rng.uniform(-v, v) * 0.5 + py *= 1.0 + rng.uniform(-v, v) * 0.5 + pz *= 1.0 + rng.uniform(-v, v) * 0.5 + ob_tmp = context.active_object + ob_tmp.scale = (px, py, pz) + _deselect_all(context) + context.view_layer.objects.active = ob_tmp + ob_tmp.select_set(True) + bpy.ops.object.transform_apply(location=False, rotation=False, scale=True) else: bpy.ops.mesh.primitive_ico_sphere_add( subdivisions=1, radius=1.0, location=location @@ -727,9 +743,24 @@ class RockGenSettings(PropertyGroup): # --- Basis --- base_shape: EnumProperty( name="Basis-Form", - items=[('CUBE', "Cube", ""), ('ICOSPHERE', "Ico-Sphere", "")], + items=[ + ('CUBE', "Wuerfel", "Gleichmaessiger Brocken"), + ('BOX', "Quader", "Laengliche oder flache Grundform - Proportionen unten"), + ('ICOSPHERE', "Ico-Sphere", "Rundlicher Brocken"), + ], default='CUBE', ) + box_proportions: FloatVectorProperty( + name="Quader-Proportionen", size=3, default=(1.6, 1.1, 0.55), + min=0.15, max=4.0, subtype='XYZ', + description=("Seitenverhaeltnis der Grundform bei 'Quader'. Flach (kleines Z) " + "ergibt Platten und Findlinge, langgestreckt ergibt Brocken. " + "Wird VOR dem Displace angewendet, damit die Verformung mitgeht"), + ) + box_variation: FloatProperty( + name="Proportions-Streuung", default=0.25, min=0.0, max=1.0, + description="Wuerfelt die Proportionen je Stein etwas anders (0 = alle gleich)", + ) subsurf_levels: IntProperty(name="Subsurf Levels", default=5, min=1, max=6) # --- Noise / Displace --- @@ -1501,7 +1532,7 @@ class ROCKGEN_OT_randomize(Operator): jitter("noise_scale_max", 0.6, 4.0) jitter("displace_strength_max", 0.2, 0.9) if s.random_shape: - s.base_shape = rng.choice(['CUBE', 'ICOSPHERE']) + s.base_shape = rng.choice(['CUBE', 'BOX', 'ICOSPHERE']) s.texture_type = rng.choice(['VORONOI', 'CLOUDS', 'DISTORTED_NOISE']) self.report({'INFO'}, "Neue Zufallswerte (Seed %d)." % s.seed) return {'FINISHED'} @@ -1626,6 +1657,9 @@ class VIEW3D_PT_rock_generator(Panel): box = layout.box() box.label(text="Basis") box.prop(settings, "base_shape") + if settings.base_shape == 'BOX': + box.prop(settings, "box_proportions") + box.prop(settings, "box_variation") box.prop(settings, "subsurf_levels") box = layout.box() diff --git a/tests/test_base_shapes.py b/tests/test_base_shapes.py new file mode 100644 index 0000000..4733467 --- /dev/null +++ b/tests/test_base_shapes.py @@ -0,0 +1,59 @@ +import bpy, sys, importlib.util, mathutils +spec=importlib.util.spec_from_file_location("srg", r"A:\eco\stylized_rock_generator\stylized_rock_generator.py") +r=importlib.util.module_from_spec(spec); sys.modules["srg"]=r; spec.loader.exec_module(r) +try: r.unregister() +except Exception: pass +r.register() +fails=[] +s=bpy.context.scene.rock_gen_settings +s.batch_count=1; s.subsurf_levels=4; s.use_modal=False +s.shading_mode='FLAT'; s.bake_vertex_cavity=True; s.set_origin_bottom=True +s.randomize_transform=False # Proportionen isoliert messen +mat=bpy.data.materials.new("P"); mat.use_nodes=True +nt=mat.node_tree; nt.nodes.clear() +o_=nt.nodes.new("ShaderNodeOutputMaterial"); b=nt.nodes.new("ShaderNodeBsdfPrincipled") +c=nt.nodes.new("ShaderNodeVertexColor"); c.layer_name="Cavity" +sep=nt.nodes.new("ShaderNodeSeparateColor"); nt.links.new(c.outputs["Color"], sep.inputs[0]) +t=nt.nodes.new("ShaderNodeRGB"); t.outputs[0].default_value=(0.66,0.64,0.60,1) +mx=nt.nodes.new("ShaderNodeMixRGB"); mx.blend_type='MULTIPLY'; mx.inputs["Fac"].default_value=1.0 +nt.links.new(t.outputs[0],mx.inputs["Color1"]); nt.links.new(sep.outputs[0],mx.inputs["Color2"]) +nt.links.new(mx.outputs[0],b.inputs["Base Color"]); b.inputs["Roughness"].default_value=0.85 +nt.links.new(b.outputs[0],o_.inputs["Surface"]) +VARS=[("Wuerfel",'CUBE',None), + ("Platte",'BOX',(2.0,1.5,0.35)), + ("Riegel",'BOX',(2.2,0.8,0.9)), + ("Ico",'ICOSPHERE',None)] +print("%-9s %-6s %s" % ("Variante","Form","Abmessungen X/Y/Z (m)")) +keep=[]; x=0.0 +for name,shape,prop in VARS: + for o in list(bpy.data.objects): bpy.data.objects.remove(o,do_unlink=True) + s.base_shape=shape; s.box_variation=0.0 + if prop: s.box_proportions=prop + s.seed=7 + bpy.ops.object.generate_rocks_batch() + ob=[o for o in bpy.context.scene.objects if o.type=='MESH'][0] + bb=ob.bound_box + d=[max(v[i] for v in bb)-min(v[i] for v in bb) for i in range(3)] + print("%-9s %-6s %.2f / %.2f / %.2f" % (name,shape,d[0],d[1],d[2])) + if name=="Platte" and not (d[2] < d[0]*0.45): fails.append("platte_nicht_flach") + if name=="Riegel" and not (d[0] > d[1]*1.5): fails.append("riegel_nicht_laenglich") + cp=ob.copy(); cp.data=ob.data.copy(); keep.append((cp,x)) + x+=2.4 +for cp,px in keep: + bpy.context.collection.objects.link(cp); cp.location=(px,0,0) + cp.data.materials.clear(); cp.data.materials.append(mat) +for o in list(bpy.context.scene.objects): + if o.type=='MESH' and o not in [k[0] for k in keep]: bpy.data.objects.remove(o,do_unlink=True) +cam_d=bpy.data.cameras.new("C"); cam=bpy.data.objects.new("C",cam_d) +bpy.context.scene.collection.objects.link(cam); bpy.context.scene.camera=cam +cam.location=(3.6,-9.0,2.6) +d=mathutils.Vector((3.6,0,0.5))-cam.location +cam.rotation_euler=d.to_track_quat('-Z','Y').to_euler() +sun=bpy.data.objects.new("S", bpy.data.lights.new("S",'SUN')) +bpy.context.scene.collection.objects.link(sun); sun.rotation_euler=(0.9,0.2,0.7); sun.data.energy=3.5 +sc=bpy.context.scene; sc.render.engine='CYCLES'; sc.cycles.samples=18 +sc.render.resolution_x=1250; sc.render.resolution_y=470 +sc.render.filepath=r"C:\Users\az319\AppData\Local\Temp\claude\A--eco-EcoGame--claude-worktrees-rock-generator-hardening-3d5f61\cf805d69-059c-4821-ac92-afae28022987\scratchpad\formen.png" +bpy.ops.render.render(write_still=True) +r.unregister() +print("ERGEBNIS:", "ALLE CHECKS OK" if not fails else "FEHLER: "+", ".join(fails))