feat(art): add storybook meadow and woodland villagers
This commit is contained in:
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,280 @@
|
||||
"""Rebuild the original woodland cast and meadow tuft with Blender 5.1+.
|
||||
|
||||
Blender --background --python tools/art/build_storybook_assets.py
|
||||
Editable .blend sources are outside Godot's import tree. Runtime GLBs contain
|
||||
vertex colors, one material and eight rigid animation parts per character.
|
||||
Coordinates below use Godot's Y-up, +Z-facing convention; export converts it.
|
||||
"""
|
||||
|
||||
import json
|
||||
import math
|
||||
import random
|
||||
from pathlib import Path
|
||||
|
||||
import bpy
|
||||
from mathutils import Vector
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
SOURCE = ROOT / "art/storybook"
|
||||
OUTPUT = ROOT / "assets/storybook"
|
||||
OUTPUT.mkdir(parents=True, exist_ok=True)
|
||||
SOURCE.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
INK = "303733"
|
||||
CREAM = "f4e5bf"
|
||||
PINK = "c98678"
|
||||
LEATHER = "654933"
|
||||
GOLD = "d9aa54"
|
||||
PIVOTS = {
|
||||
"Body": (0, 1.05, 0), "Head": (0, 1.68, 0), "Hair": (0, 1.9, 0),
|
||||
"ArmLeft": (-0.39, 1.30, 0), "ArmRight": (0.39, 1.30, 0),
|
||||
"LegLeft": (-0.18, 0.61, 0), "LegRight": (0.18, 0.61, 0),
|
||||
"Tail": (0, 0.8, -0.22),
|
||||
}
|
||||
PARTS = {}
|
||||
STATS = {}
|
||||
|
||||
|
||||
def coord(v):
|
||||
return Vector((v[0], -v[2], v[1]))
|
||||
|
||||
|
||||
def linear(hex_color):
|
||||
values = [int(hex_color[i:i + 2], 16) / 255 for i in (0, 2, 4)]
|
||||
return tuple(v / 12.92 if v <= 0.04045 else ((v + 0.055) / 1.055) ** 2.4 for v in values)
|
||||
|
||||
|
||||
def paint(obj, color, part, cloth=False):
|
||||
attr = obj.data.color_attributes.new(name="Color", type="FLOAT_COLOR", domain="CORNER")
|
||||
rgb = linear(color)
|
||||
for entry in attr.data:
|
||||
entry.color = (*rgb, 1.0 if cloth else 0.0)
|
||||
obj.data.materials.append(bpy.data.materials["StorybookPalette"])
|
||||
for poly in obj.data.polygons:
|
||||
poly.use_smooth = True
|
||||
PARTS.setdefault(part, []).append(obj)
|
||||
return obj
|
||||
|
||||
|
||||
def ellipsoid(part, center, radius, color, cloth=False, tilt=0, segments=16, rings=10):
|
||||
bpy.ops.mesh.primitive_uv_sphere_add(segments=segments, ring_count=rings, location=coord(center))
|
||||
obj = bpy.context.object
|
||||
obj.scale = (radius[0], radius[2], radius[1])
|
||||
obj.rotation_euler.y = math.radians(tilt)
|
||||
return paint(obj, color, part, cloth)
|
||||
|
||||
|
||||
def tube(part, points, radius, color, cloth=False, sides=8):
|
||||
verts, faces = [], []
|
||||
for i, point in enumerate(points):
|
||||
p = coord(point)
|
||||
tangent = coord(points[min(i + 1, len(points) - 1)]) - coord(points[max(0, i - 1)])
|
||||
tangent.normalize()
|
||||
side = tangent.cross(Vector((0, 1, 0)))
|
||||
if side.length < 0.01:
|
||||
side = tangent.cross(Vector((1, 0, 0)))
|
||||
side.normalize()
|
||||
other = tangent.cross(side).normalized()
|
||||
r = radius[i] if isinstance(radius, list) else radius
|
||||
for j in range(sides):
|
||||
a = math.tau * j / sides
|
||||
verts.append(p + r * (math.cos(a) * side + math.sin(a) * other))
|
||||
if i:
|
||||
for j in range(sides):
|
||||
a, b = (i - 1) * sides + j, (i - 1) * sides + (j + 1) % sides
|
||||
faces.append((a, b, b + sides, a + sides))
|
||||
faces.extend([tuple(reversed(range(sides))), tuple(range(len(verts) - sides, len(verts)))])
|
||||
mesh = bpy.data.meshes.new(part + "Detail")
|
||||
mesh.from_pydata(verts, [], faces)
|
||||
mesh.update()
|
||||
obj = bpy.data.objects.new(part + "Detail", mesh)
|
||||
bpy.context.collection.objects.link(obj)
|
||||
return paint(obj, color, part, cloth)
|
||||
|
||||
|
||||
def leaf_ear(part, x, bottom, height, width, fur, tilt):
|
||||
ellipsoid(part, (x, bottom + height * 0.46, -0.025), (width, height * 0.55, width * 0.57), fur, tilt=tilt)
|
||||
ellipsoid(part, (x, bottom + height * 0.48, width * 0.48), (width * 0.58, height * 0.39, 0.024), PINK, tilt=tilt, segments=12, rings=8)
|
||||
|
||||
|
||||
def reset():
|
||||
bpy.ops.object.select_all(action="SELECT")
|
||||
bpy.ops.object.delete(use_global=False)
|
||||
PARTS.clear()
|
||||
material = bpy.data.materials.get("StorybookPalette") or bpy.data.materials.new("StorybookPalette")
|
||||
material.use_nodes = True
|
||||
tree = material.node_tree
|
||||
tree.nodes.clear()
|
||||
vertex = tree.nodes.new("ShaderNodeVertexColor")
|
||||
vertex.layer_name = "Color"
|
||||
shader = tree.nodes.new("ShaderNodeBsdfPrincipled")
|
||||
shader.inputs["Roughness"].default_value = 0.88
|
||||
tree.links.new(vertex.outputs["Color"], shader.inputs["Base Color"])
|
||||
output = tree.nodes.new("ShaderNodeOutputMaterial")
|
||||
tree.links.new(shader.outputs["BSDF"], output.inputs["Surface"])
|
||||
|
||||
|
||||
def finish(name):
|
||||
objects = []
|
||||
for part, pieces in PARTS.items():
|
||||
bpy.ops.object.select_all(action="DESELECT")
|
||||
for obj in pieces:
|
||||
obj.select_set(True)
|
||||
bpy.context.view_layer.objects.active = pieces[0]
|
||||
bpy.ops.object.join()
|
||||
obj = bpy.context.object
|
||||
obj.name = part
|
||||
bpy.ops.object.transform_apply(location=False, rotation=True, scale=True)
|
||||
bpy.context.scene.cursor.location = coord(PIVOTS.get(part, (0, 0, 0)))
|
||||
bpy.ops.object.origin_set(type="ORIGIN_CURSOR")
|
||||
# Join left duplicate palette slots; one surface per animated part.
|
||||
obj.data.materials.clear()
|
||||
obj.data.materials.append(bpy.data.materials["StorybookPalette"])
|
||||
for polygon in obj.data.polygons:
|
||||
polygon.material_index = 0
|
||||
obj.data.calc_loop_triangles()
|
||||
objects.append(obj)
|
||||
bpy.ops.object.select_all(action="SELECT")
|
||||
bpy.ops.export_scene.gltf(
|
||||
filepath=str(OUTPUT / (name + ".glb")), export_format="GLB",
|
||||
use_selection=True, export_yup=True, export_animations=False,
|
||||
export_vertex_color="ACTIVE", export_all_vertex_colors=False,
|
||||
export_materials="EXPORT", export_extras=False, export_cameras=False,
|
||||
)
|
||||
bpy.context.scene.cursor.location = (0, 0, 0)
|
||||
# A useful material-colored editing view on reopening the source.
|
||||
for screen in bpy.data.screens:
|
||||
for area in screen.areas:
|
||||
if area.type == "VIEW_3D":
|
||||
area.spaces.active.shading.type = "MATERIAL"
|
||||
bpy.context.preferences.filepaths.save_version = 0
|
||||
bpy.ops.wm.save_as_mainfile(filepath=str(SOURCE / (name + ".blend")))
|
||||
STATS[name] = {"triangles": sum(len(o.data.loop_triangles) for o in objects),
|
||||
"surfaces": len(objects), "bytes": (OUTPUT / (name + ".glb")).stat().st_size}
|
||||
|
||||
|
||||
def character(species, fur, coat, scarf):
|
||||
reset()
|
||||
stout = 1.13 if species == "badger" else (0.90 if species == "rabbit" else 1.0)
|
||||
# Pear-shaped coat, soft collar, pockets, rolled hems and hand-sewn buttons.
|
||||
ellipsoid("Body", (0, 0.98, 0), (0.40 * stout, 0.50, 0.29), coat, True)
|
||||
ellipsoid("Body", (0, 1.25, 0), (0.30 * stout, 0.28, 0.23), coat, True)
|
||||
tube("Body", [(-0.22, 1.40, 0.14), (0, 1.34, 0.245), (0.23, 1.40, 0.13)], 0.065, scarf)
|
||||
ellipsoid("Body", (0.17, 1.23, 0.287), (0.085, 0.22, 0.025), scarf, tilt=-12)
|
||||
for y in (0.83, 1.0, 1.17):
|
||||
ellipsoid("Body", (-0.045, y, 0.292), (0.032, 0.032, 0.022), GOLD, segments=8, rings=6)
|
||||
for x in (-0.235, 0.235):
|
||||
ellipsoid("Body", (x, 0.83, 0.243), (0.108, 0.10, 0.022), coat, True, tilt=x * 28, segments=12, rings=8)
|
||||
tube("Body", [(x - 0.07, 0.895, 0.266), (x + 0.07, 0.895, 0.266)], 0.012, GOLD, sides=5)
|
||||
# Leather sling and tiny clasp. The inventory props remain state-controlled.
|
||||
tube("Body", [(-0.26, 1.39, 0.18), (-0.06, 1.16, 0.302), (0.21, 0.88, 0.29), (0.34, 0.73, 0.07)], 0.027, LEATHER)
|
||||
ellipsoid("Body", (0.37, 0.74, 0.04), (0.15, 0.18, 0.12), LEATHER)
|
||||
ellipsoid("Body", (0.39, 0.80, 0.15), (0.04, 0.035, 0.015), GOLD, segments=8, rings=6)
|
||||
for side, x in (("Left", -0.39), ("Right", 0.39)):
|
||||
ellipsoid("Arm" + side, (x, 1.10, 0), (0.145, 0.31, 0.145), coat, True, tilt=-x * 10)
|
||||
ellipsoid("Arm" + side, (x, 0.82, 0.018), (0.145, 0.065, 0.148), scarf)
|
||||
ellipsoid("Arm" + side, (x, 0.73, 0.024), (0.123, 0.115, 0.12), fur)
|
||||
ellipsoid("Arm" + side, (x * 0.78, 0.745, 0.075), (0.055, 0.066, 0.05), fur, segments=10, rings=6)
|
||||
for side, x in (("Left", -0.18), ("Right", 0.18)):
|
||||
ellipsoid("Leg" + side, (x, 0.41, 0), (0.137, 0.28, 0.135), "52605a")
|
||||
ellipsoid("Leg" + side, (x, 0.14, 0.075), (0.16, 0.13, 0.225), LEATHER)
|
||||
tube("Leg" + side, [(x - 0.09, 0.22, 0.19), (x + 0.09, 0.22, 0.19)], 0.014, GOLD, sides=5)
|
||||
# A broad face, cheek planes and projecting muzzle, not a capsule head.
|
||||
head_w = 0.37 * stout
|
||||
ellipsoid("Head", (0, 1.76, 0), (head_w, 0.34, 0.29), fur)
|
||||
if species == "fox":
|
||||
for s in (-1, 1):
|
||||
tube("Head", [(s * 0.23, 1.99, 0), (s * 0.29, 2.19, -0.03), (s * 0.33, 2.39, -0.055)], [0.135, 0.095, 0.005], fur, sides=8)
|
||||
tube("Head", [(s * 0.24, 2.05, 0.105), (s * 0.30, 2.25, 0.02)], [0.06, 0.006], PINK, sides=6)
|
||||
ellipsoid("Head", (s * 0.19, 1.63, 0.21), (0.19, 0.15, 0.16), CREAM, tilt=s * 18)
|
||||
ellipsoid("Head", (0, 1.69, 0.29), (0.16, 0.105, 0.22), CREAM)
|
||||
tube("Tail", [(0, 0.81, -0.2), (0.17, 0.52, -0.49), (0.44, 0.54, -0.76), (0.56, 0.82, -0.76)], [0.12, 0.23, 0.24, 0.17], fur, sides=12)
|
||||
tube("Tail", [(0.53, 0.74, -0.78), (0.57, 0.95, -0.72), (0.48, 1.12, -0.60)], [0.195, 0.15, 0.003], CREAM, sides=12)
|
||||
elif species == "rabbit":
|
||||
leaf_ear("Head", -0.19, 1.98, 0.70, 0.10, fur, -12)
|
||||
leaf_ear("Head", 0.21, 1.96, 0.62, 0.12, fur, 22)
|
||||
ellipsoid("Tail", (0, 0.74, -0.36), (0.19, 0.18, 0.19), CREAM)
|
||||
elif species == "badger":
|
||||
for s in (-1, 1):
|
||||
ellipsoid("Head", (s * 0.30, 2.01, -0.02), (0.125, 0.145, 0.09), INK)
|
||||
ellipsoid("Head", (s * 0.30, 2.025, 0.053), (0.07, 0.085, 0.025), CREAM)
|
||||
ellipsoid("Head", (s * 0.19, 1.82, 0.226), (0.10, 0.25, 0.093), INK, tilt=s * -18)
|
||||
ellipsoid("Head", (0, 1.68, 0.32), (0.22, 0.14, 0.16), CREAM)
|
||||
ellipsoid("Tail", (0, 0.67, -0.34), (0.13, 0.25, 0.16), fur)
|
||||
else:
|
||||
for s in (-1, 1):
|
||||
ellipsoid("Head", (s * 0.31, 1.94, 0), (0.10, 0.115, 0.075), fur)
|
||||
ellipsoid("Head", (s * 0.32, 1.94, 0.063), (0.054, 0.064, 0.025), PINK)
|
||||
tube("Tail", [(0, 0.80, -0.22), (0, 0.46, -0.52), (0.18, 0.22, -0.78), (0.37, 0.19, -0.81)], [0.16, 0.15, 0.09, 0.008], fur, sides=10)
|
||||
if species != "fox":
|
||||
for s in (-1, 1):
|
||||
ellipsoid("Head", (s * 0.09, 1.64, 0.282), (0.15, 0.10, 0.115), CREAM)
|
||||
nose_z = 0.48 if species == "fox" else 0.408
|
||||
ellipsoid("Head", (0, 1.706, nose_z), (0.058, 0.043, 0.047), PINK if species == "rabbit" else INK, segments=12, rings=8)
|
||||
for s in (-1, 1):
|
||||
eye_x = s * (0.16 if species != "badger" else 0.20)
|
||||
eye_z = 0.33 if species == "badger" else 0.26
|
||||
ellipsoid("Head", (eye_x, 1.845, eye_z), (0.049, 0.061, 0.037), INK)
|
||||
ellipsoid("Head", (eye_x - 0.012, 1.868, eye_z + 0.03), (0.013, 0.018, 0.012), CREAM, segments=8, rings=6)
|
||||
tube("Head", [(eye_x - 0.047, 1.954, eye_z), (eye_x, 1.97 + s * 0.012, eye_z + 0.01), (eye_x + 0.042, 1.95, eye_z)], 0.017, CREAM if species == "badger" else INK, sides=6)
|
||||
ellipsoid("Head", (s * 0.26, 1.688, 0.248), (0.046, 0.024, 0.025), PINK, segments=10, rings=6)
|
||||
tube("Head", [(0, 1.659, nose_z - 0.008), (s * 0.06, 1.611, nose_z - 0.045), (s * 0.117, 1.636, nose_z - 0.085)], 0.009, INK, sides=5)
|
||||
# Accessories give each silhouette a small joke and a practical occupation.
|
||||
if species == "badger":
|
||||
for s in (-1, 1):
|
||||
points = [(s * 0.20 + math.cos(a * math.tau / 20) * 0.09, 1.845 + math.sin(a * math.tau / 20) * 0.08, 0.373) for a in range(21)]
|
||||
tube("Head", points, 0.009, GOLD, sides=5)
|
||||
tube("Head", [(-0.11, 1.85, 0.373), (0, 1.88, 0.39), (0.11, 1.85, 0.373)], 0.011, GOLD, sides=5)
|
||||
if species in ("otter", "rabbit"):
|
||||
hat_color = "d3b66f" if species == "rabbit" else "777d52"
|
||||
ellipsoid("Hair", (0, 2.025, -0.01), (0.44, 0.055, 0.34), hat_color)
|
||||
ellipsoid("Hair", (0, 2.09, -0.035), (0.28, 0.14, 0.245), hat_color)
|
||||
tube("Hair", [(-0.25, 2.063, 0.09), (0, 2.06, 0.23), (0.24, 2.063, 0.09)], 0.026, scarf)
|
||||
ellipsoid("Hair", (0.25, 2.15, 0.15), (0.038, 0.12, 0.065), "8f9c5a", tilt=40, segments=10, rings=6)
|
||||
else:
|
||||
# A little forelock above the brow, merged into the existing animated slot.
|
||||
ellipsoid("Hair", (-0.04, 2.04, 0.05), (0.11, 0.07, 0.15), fur, tilt=-25, segments=12, rings=8)
|
||||
finish(species)
|
||||
|
||||
|
||||
def grass():
|
||||
reset()
|
||||
rng = random.Random(7103)
|
||||
verts, faces, uv = [], [], []
|
||||
for blade in range(7):
|
||||
angle = blade * 2.399
|
||||
center = Vector((math.cos(angle) * 0.15, math.sin(angle) * 0.15, 0))
|
||||
width = rng.uniform(0.025, 0.055)
|
||||
height = rng.uniform(0.8, 1.0)
|
||||
sideways = Vector((math.cos(angle), math.sin(angle), 0))
|
||||
bend = Vector((math.cos(angle + 0.8), math.sin(angle + 0.8), 0)) * rng.uniform(0.045, 0.12)
|
||||
start = len(verts)
|
||||
for level in (0, 0.42, 0.77):
|
||||
for side in (-1, 1):
|
||||
verts.append(center + Vector((0, 0, height * level)) + bend * level * level + sideways * side * width * (1 - level))
|
||||
uv.append(((side + 1) / 2, level))
|
||||
verts.append(center + Vector((0, 0, height)) + bend)
|
||||
uv.append((0.5, 1))
|
||||
for level in range(2):
|
||||
a = start + level * 2
|
||||
faces.extend([(a, a + 1, a + 2), (a + 1, a + 3, a + 2)])
|
||||
faces.append((start + 4, start + 5, start + 6))
|
||||
mesh = bpy.data.meshes.new("MeadowTuft")
|
||||
mesh.from_pydata(verts, [], faces)
|
||||
mesh.update()
|
||||
layer = mesh.uv_layers.new(name="UVMap")
|
||||
for loop in mesh.loops:
|
||||
layer.data[loop.index].uv = uv[loop.vertex_index]
|
||||
obj = bpy.data.objects.new("Grass", mesh)
|
||||
bpy.context.collection.objects.link(obj)
|
||||
paint(obj, "ffffff", "Grass")
|
||||
finish("meadow_grass")
|
||||
|
||||
|
||||
character("fox", "c17b42", "568b82", "d6934d")
|
||||
character("rabbit", "d4c5a5", "839d70", "c68061")
|
||||
character("badger", "b8b9a8", "7c879c", "c5aa6d")
|
||||
character("otter", "947055", "b77c58", "8caa98")
|
||||
grass()
|
||||
(OUTPUT / "mesh_budget.json").write_text(json.dumps(STATS, indent=2) + "\n")
|
||||
print("STORYBOOK ASSET BUDGET", json.dumps(STATS))
|
||||
@@ -0,0 +1,171 @@
|
||||
extends SceneTree
|
||||
## Reproducible native-renderer character and runtime camera review.
|
||||
|
||||
const OUTPUT := "res://docs/baselines/"
|
||||
const CAPTURE_SIZE := Vector2i(1600, 900)
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
call_deferred("_run")
|
||||
|
||||
|
||||
func _run() -> void:
|
||||
root.size = CAPTURE_SIZE
|
||||
if OS.get_cmdline_user_args().has("--cast"):
|
||||
await _capture_cast()
|
||||
else:
|
||||
await _capture_world()
|
||||
quit(0)
|
||||
|
||||
|
||||
func _capture_world() -> void:
|
||||
var main := load("res://main.tscn").instantiate() as Node3D
|
||||
main.get_node("SimulationManager").set("debug_logs", false)
|
||||
root.add_child(main)
|
||||
await process_frame
|
||||
var manager := main.get_node("SimulationManager")
|
||||
manager.set_process(false)
|
||||
var cycle := main.get_node("JajceWorld/DayNightCycle")
|
||||
cycle.set_process(false)
|
||||
cycle.call("_apply_light_rotation", 0.40)
|
||||
cycle.call("_apply_environment", 0.40)
|
||||
var demo := main.get_node("DemoController")
|
||||
if demo.get("debug_overlay_visible"):
|
||||
demo.call("toggle_debug_overlay")
|
||||
for child in main.get_children():
|
||||
if child is CanvasLayer:
|
||||
child.hide()
|
||||
var rig := main.get_node("CameraRig") as Node3D
|
||||
rig.set_physics_process(false)
|
||||
rig.set_process_unhandled_input(false)
|
||||
var player := main.get_node("Player") as CharacterBody3D
|
||||
for _frame in 30:
|
||||
await process_frame
|
||||
player.set_physics_process(false)
|
||||
var camera := root.get_camera_3d()
|
||||
camera.reparent(main)
|
||||
camera.global_position = player.global_position + Vector3(7.5, 5.5, 10.5)
|
||||
camera.look_at(player.global_position + Vector3(0, 1.1, 0))
|
||||
camera.fov = 48.0
|
||||
for _frame in 90:
|
||||
await process_frame
|
||||
await _save("storybook_gameplay.png")
|
||||
camera.global_position = Vector3(38, 24, 44)
|
||||
camera.look_at(Vector3(-4, 3, 0))
|
||||
camera.fov = 53.0
|
||||
for _frame in 60:
|
||||
await process_frame
|
||||
await _save("storybook_valley.png")
|
||||
var world := main.get_node("JajceWorld") as JajceWorld
|
||||
var samples: Array[Dictionary] = []
|
||||
for quality in [
|
||||
JajceWorld.PresentationQuality.HIGH,
|
||||
JajceWorld.PresentationQuality.BALANCED,
|
||||
JajceWorld.PresentationQuality.LOW
|
||||
]:
|
||||
world.apply_presentation_quality(quality)
|
||||
for _frame in 60:
|
||||
await process_frame
|
||||
var frame_times: Array[float] = []
|
||||
for _frame in 120:
|
||||
var start := Time.get_ticks_usec()
|
||||
await process_frame
|
||||
frame_times.append(float(Time.get_ticks_usec() - start) / 1000.0)
|
||||
frame_times.sort()
|
||||
(
|
||||
samples
|
||||
. append(
|
||||
{
|
||||
"profile": world.get_active_presentation_quality_name(),
|
||||
"frame_wall_p50_ms": frame_times[60],
|
||||
"frame_wall_p95_ms": frame_times[114],
|
||||
"draw_calls":
|
||||
Performance.get_monitor(Performance.RENDER_TOTAL_DRAW_CALLS_IN_FRAME),
|
||||
"primitives":
|
||||
Performance.get_monitor(Performance.RENDER_TOTAL_PRIMITIVES_IN_FRAME),
|
||||
"objects": Performance.get_monitor(Performance.RENDER_TOTAL_OBJECTS_IN_FRAME),
|
||||
"active_npcs": main.get_node("ActiveNPCs").get_child_count(),
|
||||
}
|
||||
)
|
||||
)
|
||||
var file := FileAccess.open(OUTPUT + "storybook_render_metrics.json", FileAccess.WRITE)
|
||||
(
|
||||
file
|
||||
. store_string(
|
||||
(
|
||||
(
|
||||
JSON
|
||||
. stringify(
|
||||
{
|
||||
"renderer": RenderingServer.get_current_rendering_driver_name(),
|
||||
"resolution": "1600x900",
|
||||
"workload":
|
||||
"Paused static valley; wall-frame latency includes vsync, not isolated GPU time",
|
||||
"samples": samples
|
||||
},
|
||||
"\t"
|
||||
)
|
||||
)
|
||||
+ "\n"
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
func _capture_cast() -> void:
|
||||
var stage := Node3D.new()
|
||||
root.add_child(stage)
|
||||
var environment := WorldEnvironment.new()
|
||||
environment.environment = Environment.new()
|
||||
var env := environment.environment
|
||||
env.background_mode = Environment.BG_COLOR
|
||||
env.background_color = Color("d8e3d5")
|
||||
env.ambient_light_source = Environment.AMBIENT_SOURCE_COLOR
|
||||
env.ambient_light_color = Color("bbd2d3")
|
||||
env.ambient_light_energy = 0.65
|
||||
env.tonemap_mode = Environment.TONE_MAPPER_FILMIC
|
||||
stage.add_child(environment)
|
||||
var light := DirectionalLight3D.new()
|
||||
light.rotation_degrees = Vector3(-42, -32, 0)
|
||||
light.light_color = Color("fff0cd")
|
||||
light.light_energy = 1.3
|
||||
light.shadow_enabled = true
|
||||
stage.add_child(light)
|
||||
var floor_mesh := MeshInstance3D.new()
|
||||
var plane := PlaneMesh.new()
|
||||
plane.size = Vector2(200, 200)
|
||||
var material := StandardMaterial3D.new()
|
||||
material.albedo_color = Color("bdcbb5")
|
||||
material.roughness = 1.0
|
||||
plane.material = material
|
||||
floor_mesh.mesh = plane
|
||||
stage.add_child(floor_mesh)
|
||||
for index in 4:
|
||||
var actor := load("res://player/PlayerVisual.tscn").instantiate() as Node3D
|
||||
stage.add_child(actor)
|
||||
actor.set_process(false)
|
||||
AnimalAppearance.apply_to(actor, index)
|
||||
actor.get_node("Sword").hide()
|
||||
actor.position = Vector3((float(index) - 1.5) * 2.0, 0, 0)
|
||||
actor.rotation_degrees.y = -12.0 + float(index) * 5.0
|
||||
var camera := Camera3D.new()
|
||||
stage.add_child(camera)
|
||||
camera.position = Vector3(2.2, 3.1, 10.5)
|
||||
camera.look_at(Vector3(0, 1.25, 0))
|
||||
camera.projection = Camera3D.PROJECTION_ORTHOGONAL
|
||||
camera.size = 6.2
|
||||
camera.current = true
|
||||
for _frame in 60:
|
||||
await process_frame
|
||||
await _save("storybook_cast.png")
|
||||
stage.queue_free()
|
||||
await process_frame
|
||||
|
||||
|
||||
func _save(filename: String) -> void:
|
||||
await RenderingServer.frame_post_draw
|
||||
var capture := root.get_texture().get_image()
|
||||
assert(not capture.is_empty())
|
||||
var error := capture.save_png(OUTPUT + filename)
|
||||
assert(error == OK)
|
||||
print("[TOOL] Saved " + OUTPUT + filename)
|
||||
@@ -0,0 +1 @@
|
||||
uid://bqcj3uman431w
|
||||
@@ -0,0 +1,12 @@
|
||||
extends SceneTree
|
||||
## Run after Blender export and Godot import; the particle field requires a Mesh.
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
var scene := load("res://assets/storybook/meadow_grass.glb") as PackedScene
|
||||
var instance := scene.instantiate()
|
||||
var grass := instance.find_child("Grass", true, false) as MeshInstance3D
|
||||
assert(grass != null)
|
||||
var error := ResourceSaver.save(grass.mesh, "res://assets/storybook/meadow_grass.res")
|
||||
instance.free()
|
||||
quit(0 if error == OK else 1)
|
||||
@@ -0,0 +1 @@
|
||||
uid://blkknmqjyo6uj
|
||||
Reference in New Issue
Block a user