"""Etude originale NEXIA. Execution dans un Blender neuf, jamais dans un projet ouvert.""" import bpy, math, json, hashlib from pathlib import Path from mathutils import Vector ROOT=Path(__file__).resolve().parent OUT=ROOT/'assets'; OUT.mkdir(exist_ok=True) bpy.ops.object.select_all(action='SELECT'); bpy.ops.object.delete(use_global=False) scene=bpy.context.scene scene.render.engine='CYCLES'; scene.cycles.samples=48 scene.cycles.use_denoising=True try: prefs=bpy.context.preferences.addons['cycles'].preferences prefs.compute_device_type='OPTIX'; prefs.get_devices() for dev in prefs.devices: dev.use=dev.type!='CPU' if any(d.use for d in prefs.devices): scene.cycles.device='GPU' except Exception: pass scene.render.resolution_x=1600; scene.render.resolution_y=1000; scene.render.resolution_percentage=100 scene.render.image_settings.file_format='PNG' scene.view_settings.view_transform='AgX' scene.render.film_transparent=False scene.world.use_nodes=True scene.world.node_tree.nodes.get('Background').inputs[0].default_value=(0.30,0.39,0.54,1) scene.world.node_tree.nodes.get('Background').inputs[1].default_value=.22 scene.unit_settings.system='METRIC' def mat(name,color,rough=.5,metal=0,stone=False): m=bpy.data.materials.new(name);m.use_nodes=True n=m.node_tree.nodes;p=n.get('Principled BSDF');p.inputs['Base Color'].default_value=(*color,1);p.inputs['Roughness'].default_value=rough;p.inputs['Metallic'].default_value=metal if stone: noise=n.new('ShaderNodeTexNoise');noise.inputs['Scale'].default_value=38;noise.inputs['Detail'].default_value=3 bump=n.new('ShaderNodeBump');bump.inputs['Strength'].default_value=.20;bump.inputs['Distance'].default_value=.025 m.node_tree.links.new(noise.outputs['Fac'],bump.inputs['Height']);m.node_tree.links.new(bump.outputs['Normal'],p.inputs['Normal']) return m stone=mat('Calcaire ivoire',(0.64,.55,.42),.72,stone=True) dark=mat('Pierre anthracite',(.045,.058,.061),.55,stone=True) bronze=mat('Bronze brosse',(.33,.13,.045),.29,.82) cobalt=mat('Laque bleu profond',(.012,.05,.18),.25,.25) def cube(name,loc,size,material,bevel=.025): bpy.ops.mesh.primitive_cube_add(size=1,location=loc);o=bpy.context.object;o.name=name;o.dimensions=size;bpy.ops.object.transform_apply(location=False,rotation=False,scale=True);o.data.materials.append(material) if bevel: mod=o.modifiers.new('Aretes adoucies','BEVEL');mod.width=bevel;mod.segments=3 o.modifiers.new('Normales','WEIGHTED_NORMAL') return o cube('Sol',(0,1,-.16),(35,36,.3),stone) cube('Mur du fond',(0,9,4),(22,.4,8),stone) # Colonnade ouverte : la lumière et les ombres font lire les volumes. for i,y in enumerate([-2,1,4,7]): for x in [-4.5,4.5]: cube('Pilier_%s_%s'%(i,x),(x,y,3),(.65,.8,6),stone,.06) cube('Traverse_%s'%i,(0,y,6),(9.7,.9,.42),stone,.05) cube('Mur lateral',(-5.1,3.4,3),(.4,12,6),stone) cube('Banc',(3.3,5.7,.55),(2.8,.8,.24),dark,.035) cube('Pied_banc_A',(2.3,5.7,.23),(.2,.6,.45),dark) cube('Pied_banc_B',(4.3,5.7,.23),(.2,.6,.45),dark) cube('Socle',(0,1.3,.45),(2.4,1.5,.9),dark,.05) # Anneau sculptural asymétrique, objet unique et dimensions contrôlables. verts=[]; faces=[] for i in range(192): u=i*2*math.pi/192 for j in range(24): v=j*2*math.pi/24 rad=.17*(1+.24*math.sin(u)) verts.append(((1.1+rad*math.cos(v))*math.cos(u),1.3+rad*math.sin(v),2.55+(1.55+rad*math.cos(v))*math.sin(u))) for i in range(192): for j in range(24): faces.append((i*24+j,((i+1)%192)*24+j,((i+1)%192)*24+(j+1)%24,i*24+(j+1)%24)) mesh=bpy.data.meshes.new('Anneau_mesh');mesh.from_pydata(verts,[],faces);mesh.update() obj=bpy.data.objects.new('Sculpture',mesh);bpy.context.collection.objects.link(obj);obj.data.materials.append(bronze) for p in mesh.polygons:p.use_smooth=True # Repère asymétrique pour distinguer les vues. cube('Encoche_repere',(.89,1.03,1.62),(.22,.27,.20),bronze,.02) sun_data=bpy.data.lights.new('Soleil','SUN');sun_data.energy=2.1;sun_data.angle=.055;sun_data.color=(1,.81,.61) sun=bpy.data.objects.new('Soleil',sun_data);bpy.context.collection.objects.link(sun);sun.rotation_euler=(math.radians(44),math.radians(-27),math.radians(-32)) ld=bpy.data.lights.new('Ciel ouvert','AREA');ld.energy=750;ld.shape='DISK';ld.size=9;ld.color=(.73,.83,1) lo=bpy.data.objects.new('Ciel ouvert',ld);bpy.context.collection.objects.link(lo);lo.location=(1,-2,8) def camera(name,pos,target,lens): data=bpy.data.cameras.new(name);data.lens=lens;data.sensor_width=36 o=bpy.data.objects.new(name,data);bpy.context.collection.objects.link(o);o.location=pos;o.rotation_euler=(Vector(target)-o.location).to_track_quat('-Z','Y').to_euler();return o cameras=[camera('Plan ensemble',(7.2,-11,4.2),(0,2,2.3),45),camera('Plan rapproche',(-2.8,-5.5,2.8),(0,1.3,2.4),58),camera('Contrechamp',(3.8,6.4,3.3),(0,1.3,2.3),32)] scene.camera=cameras[0] geometry=lambda:hashlib.sha256(json.dumps([(o.name,[list(v.co) for v in o.data.vertices],list(o.matrix_world)) for o in scene.objects if o.type=='MESH'],default=list,sort_keys=True).encode()).hexdigest() baseline=geometry(); records=[] for cam,name in zip(cameras,['ensemble','rapproche','contrechamp']): scene.camera=cam;scene.render.filepath=str(OUT/(name+'.png'));bpy.ops.render.render(write_still=True) records.append({'name':name,'camera':cam.name,'lens_mm':cam.data.lens,'geometry_sha256':geometry(),'file':name+'.png'}) scene.camera=cameras[0];obj.data.materials[0]=cobalt scene.render.filepath=str(OUT/'matiere.png');bpy.ops.render.render(write_still=True) records.append({'name':'matiere','camera':scene.camera.name,'geometry_sha256':geometry(),'file':'matiere.png'}) obj.data.materials[0]=bronze;scene.camera=cameras[0] bpy.ops.wm.save_as_mainfile(filepath=str(OUT/'atrium.blend')) (ROOT/'preuves.json').write_text(json.dumps({'date':'2026-09-16','blender':bpy.app.version_string,'engine':scene.render.engine,'device':scene.cycles.device,'samples':scene.cycles.samples,'dimensions':[1600,1000],'method':'Scene ecrite par Codex puis executee dans Blender. Aucun imagegen, aucune reconstruction depuis une photo.','mesh_count':len([o for o in scene.objects if o.type=='MESH']),'same_geometry':all(r['geometry_sha256']==baseline for r in records),'views':records},indent=2),encoding='utf-8') print('NEXIA_STUDY_COMPLETE')