bradyajohnston/molecularnodes/mn-render

Trust tier 2
  • community
LLM review pending
Source check pending

Checking that the SKILL.md file is still in the repository.

How to render images and animations of molecules from a Python script with the molecularnodes package (Canvas, engines, framing, styles, materials, colour, trajectories, deterministic renders for tests). Load before writing or debugging any script that calls mn.Canvas, snapshot, animation or record.

Install

npx vskill@latest install bradyajohnston/molecularnodes/mn-render

SKILL.md

Rendering with Molecular Nodes from a Python script

1. Mental model

  • One `Canvas`, many entities. mn.Canvas wraps the active Blender scene: engine,

resolution, camera, world lighting, compositor and output. Molecules are mn.Molecule entities that you load, style and then frame with canvas.look_at.

  • Create the Canvas before loading anything. It loads the Molecular Nodes scene

preset (camera, lights, world, compositor) into an empty scene. Calling mn.Canvas() again is safe: it binds to the existing scene and only reloads the preset when the scene holds no molecules.

  • Styles are node trees. add_style builds a small node branch per call; the

render shows whatever the evaluated geometry is. look_at frames that evaluated geometry, so a molecule with one chain styled is framed on that chain.

  • Units. Positions are Å times world_scale = 0.1, so 1 Å is 0.1 Blender units.
  • There is no mn.Trajectory any more; MD topologies and trajectories load into

mn.Molecule as well.

2. Environment for a script

Always via uv run (never bare python). In a script that imports bpy directly:

python
import os, tempfile
os.environ.setdefault(
"BLENDER_USER_EXTENSIONS", tempfile.mkdtemp()
) # before import bpy
import bpy # mathutils is importable only after this
import molecularnodes as mn
canvas = mn.Canvas(mn.scene.Cycles(samples=64, device="CPU"), resolution=(1200, 900))
canvas.compositor.device = (
"CPU" # Blender 5 defaults the compositor to GPU; aborts without one
)

Without the BLENDER_USER_EXTENSIONS line, an installed MN extension's bundled wheels shadow the venv. tests/conftest.py does the same thing. Keep scratch scripts outside the repo.

Inside Blender with the installed extension (blender -b --python script.py), the package is import bl_ext.blender_org.molecularnodes as mn; import molecularnodes fails there. Install headless with blender --online-mode --command extension install --enable molecularnodes (without --online-mode Blender refuses to reach the platform). Do not call bpy.ops.wm.read_factory_settings() to reset between renders: it has been reported to strip the extension's bundled wheels mid-run, so the next Molecule.load() fails on a missing biotite file. Delete the default objects by hand, or use one Blender process per render. Don't name a scratch script after a standard-library module (inspect.py, code.py, types.py, ...). Importing bpy imports that module, finds the script instead, and Blender initialises twice and aborts with Check failed: !IsGoogleLoggingInitialized().

3. Minimal still

python
mol = mn.Molecule.fetch("4ozs") # .bcif from RCSB, cached
mol.add_style("cartoon", material=mn.material.AmbientOcclusion())
canvas.look_at(mol, viewpoint="front")
canvas.snapshot("4ozs.png") # returns an IPython Image for PNG/JPEG

snapshot(path=None, frame=None, file_format="PNG", render_scale=100). With no path it renders to a temp file and returns the image (notebooks display it). frame= renders one scene frame and restores the current frame afterwards. TIFF/EXR write the file but return None. The canonical docs versions are in docs/api/index.qmd and docs/api/rendering.qmd.

4. Canvas settings

membermeaning
enginemn.scene.EEVEE(samples=64, raytracing=True) or mn.scene.Cycles(samples=256, device="GPU", denoise=True, denoise_gpu=True); also accepts "EEVEE" / "CYCLES". Constructor values are written to the scene on assignment.
resolution(x, y) pixels. render_scale is the percentage.
samplessamples on the active engine.
transparentfilm transparency (alpha background).
backgroundworld background RGBA; shortcut into the MN_world_shader node.
world.hdri_strengthlighting strength of the preset HDRI.
view_transform, exposure, gamma, lookcolour management. Blender defaults to AgX; use "Standard" when colours must come out as specified. mn.Canvas(view_transform="Standard") sets it on construction.
passesrender passes: combined, z, mist, normal, position, vector, diffuse_color, emit, environment, ambient_occlusion.
frame, frame_start, frame_end, frame_range, fpstimeline. Setting frame uses frame_set, so trajectories update.
cameralens, clip_start, clip_end, rotation (XYZ Euler in degrees), basis, set_viewpoint(...), frame_points(points, margin).
compositordevice, precision, denoise settings, reset(), clear(), add_annotations().
  • canvas.clear() deletes every object except cameras and lights (including the

preset backdrop), purges orphans recursively, and keeps engine, world, compositor and render settings. canvas.load_preset() reloads the whole preset.

  • Cycles(device="GPU") tries OPTIX, CUDA, METAL, HIP, ONEAPI and falls back to CPU

with a warning. EEVEE needs a GPU; the GitHub runners do not have one, so CI-facing scripts use Cycles CPU.

  • world.reset() and compositor.reset() remove the preset nodes: after them

canvas.background raises ValueError and the annotation overlay is gone (compositor.add_annotations() restores it).

5. Loading entities

python
mol = mn.Molecule.fetch("9MD2") # format=".bcif", database="rcsb"
af = mn.Molecule.fetch("Q8W3K0", database="alphafold")
pdb = mn.Molecule.load("path/protein.pdb", style="cartoon") # single structure
traj = mn.Molecule.load("topol.tpr", "traj.xtc", name="md") # MD topology + coordinates
u_mol = mn.Molecule(u) # from an MDAnalysis Universe

fetch(code, format=".bcif", cache=download.CACHE_DIR, database="rcsb"). load routes single files through biotite and topology plus coordinates through MDAnalysis; style= is optional and defaults to no style, leaving the tree empty. In tests use the fetch fixture instead, which loads from tests/data and never downloads.

6. Styles, materials, colour

python
mol.add_style(
"surface", selection="protein", material=mn.material.Flat(), color="common"
)
mol.add_style("ball_and_stick", selection="not protein", color=(1.0, 0.5, 0.0, 1.0))
mol.add_style(
lambda: mg.StyleCartoon(quality=5, loop_radius=0.6), color=lambda: mg.ColorRainbow()
)
  • Style names: spheres, cartoon, ribbon, surface, sticks, ball_and_stick.

Extra kwargs go to the style node's inputs (quality, scale, loop_radius, sphere, ...); unknown names raise TypeError, unknown style strings ValueError. add_style returns the molecule, so calls chain.

  • Callable style (lambda: mg.StyleX(...)) builds the node itself, so it cannot be

combined with selection, material or kwargs (TypeError); color, assembly and name are fine.

  • Selection is an existing boolean attribute name first, then an MDAnalysis phrase

(stored as a managed sel_N attribute), an AtomGroup, or a callable returning a boolean socket (lambda: mg.IsPeptide() & mg.IsSideChain()).

  • `assembly=True` instances the style over the file's biological assembly.
  • Materials. Presets in mn.material: Default(roughness, ao_distance, ao_exponent),

AmbientOcclusion(distance, exponent), Flat(outline, threshold, thickness) (no outline unless outline=True), Squishy(subsurface_scale, roughness), Transparent(transparency, fresnel, outline_color). Each instantiation builds an independent datablock ("Flat.001"), and its parameters are live: mat.distance = 0.15 changes the next render. Passing the string material="Flat" reuses one shared datablock, so edits affect every style using it. "Fresnel Outline" exists only as mn.material.append_material("Fresnel Outline"). Transparent needs the blended surface method, which its recipe sets. To swap the material on an existing style: style.i.material.default_value = mat.material.

  • Colour. color= takes "common"/"default" (elements, carbons random per

chain), "plddt", the name of an existing colour attribute, an RGBA sequence, or a callable returning a colour socket. Any other string warns and applies nothing (this replaced a silent black render). Arrays go on directly with mol["Color"] = rgba_array.

  • Colouring part of a representation. Do not add the same style once per selection

to colour regions (a cartoon per section, sticks per nucleotide): each style builds its own geometry, so a cartoon or ribbon is cut at every boundary (blocky helix ends, stubs at the cuts). Add each style once and colour it from a per-atom attribute: ``python mol["Color"][mol.universe.select_atoms("chainID A and resid 19-26").ix, :3] = ( 0.1, 0.7, 0.7, ) # color=None reads Color; or store under another name and pass color="name" mol.add_style("cartoon") mol.add_style("sticks", selection="chainID A and resid 19-26") ` A new attribute needs atype="FLOAT_COLOR": mol["name"] = rgba infers a FLOAT4, which add_style` rejects (it warns and applies nothing). Cartoon colour follows the backbone atoms, so colouring only side-chain atoms leaves the cartoon unchanged.

  • Spheres in EEVEE. Point clouds are ray-traced only by Cycles; on other engines

add_style switches StyleSpheres from sphere="Point" to "Instance" for you. If you build the tree by hand, set sphere="Instance" or "Mesh" yourself, and use instances whenever point clouds with different materials are joined.

7. Framing

python
canvas.look_at(mol) # keep current direction, fit the subject
canvas.look_at(
mol, viewpoint="top", margin=0.15
) # front, back, top, bottom, left, right, default
canvas.look_at(mol, viewpoint=(90, 0, 45)) # XYZ Euler degrees
canvas.look_at(mol.get_view("chainID A and resid 1-40"))
canvas.look_at(points_xyz) # any (N, 3) array in world units
  • look_at(target, viewpoint=None, margin=0.05) fits the camera to the evaluated

geometry without changing where it points; margin=0 is exact, negative crops.

  • camera.lens changes after look_at invalidate the fit: set the lens first, or call

look_at again.

  • Stale camera basis. set_viewpoint writes rotation_euler, but the fit reads

matrix_world, which only updates with the depsgraph. look_at(entity_or_object, viewpoint=...) is safe because reading the evaluated geometry updates it first. look_at(points_array, viewpoint=...), or set_viewpoint(...) followed by a separate look_at, fits against the previous orientation and renders empty or off-centre (measured 2026-09-18). Call bpy.context.view_layer.update() between the two until look_at does it itself.

  • frame_points extends clip_end when the subject would be clipped; if a hand-placed

camera renders nothing, check camera.clip_end first.

  • Rotating the subject instead of the camera: leave the camera at "default" and

rotate the object, or use the viewpoint tuple in a loop for orbits (section 9).

8. Trajectories

Per-molecule playback properties, all stored on the Blender object: frame, subframes, offset, average, correct_periodic, interpolate. A trajectory with n universe frames and subframes = s spans n * (s + 1) - 1 scene frames.

python
traj = mn.Molecule(u)
# secondary structure per frame, if cartoon/ribbon needs it
traj.calculations.dssp(updating=True)
traj.subframes, traj.interpolate, traj.average = 1, True, 1
traj.add_style("cartoon", color="common")
canvas.fps = 24
canvas.frame_range = (0, u.trajectory.n_frames * 2 - 1)
canvas.frame = 0
canvas.look_at(traj, viewpoint="top", margin=0.35)

Molecule.frames_to_collection(start=0, stop=None, step=1) bakes frames into a collection for the Animate Frames node when the timeline route is not wanted. set_frame is what the frame-change handler calls; user code sets canvas.frame.

9. Animations and image sequences

Three routes, pick by what changes between frames:

This is the start of the file. Read the full SKILL.md on GitHub.

Security

  • Security scanPassed, 100 of 100Pattern checks on the skill's files.
  • Model reviewNot runA model review is scheduled for the next rescan.

Quality evaluation

  • Task evaluationNot runNo evaluation yet. Run one in Skill Studio.

Source

Popularity

Repository stars
1,378
Repository forks
133
7-day trend score
41

Stars and forks are for the whole GitHub repository, not this skill alone. Not refreshed yet.

Works with

Any agent vskill installs to: 56 targets in all. These 39 have a page on this site.

  • Claude Code
  • Cursor
  • GitHub Copilot
  • Windsurf
  • Codex
  • Gemini CLI
  • Cline
  • Amp
  • Roo Code
  • Goose
  • Continue
  • Kilo Code
  • Junie
  • Trae
  • OpenHands
  • Qwen Code
  • AI Agent
  • Tabnine Chat
  • Sourcegraph Cody
  • Amazon Q
  • Firebase Genkit
  • Sweep
  • Devin
  • Phind
  • Mentat
  • Claude Desktop
  • Aider
  • GPT Pilot
  • Plandex
  • bolt.new
  • v0
  • Replit Agent
  • Anse
  • TypingMind
  • BetterChatGPT
  • LibreChat
  • Warp AI
  • Pieces
  • Fig AI
Embed badge
vskill VERIFIED badge
[![vskill VERIFIED](https://verified-skill.com/api/v1/skills/bradyajohnston/molecularnodes/mn-render/badge)](https://verified-skill.com/skills/bradyajohnston/molecularnodes/mn-render)

Something wrong with this skill or its page? Tell us.