feat(site): blocos 3D da marca, novas ofertas e hero mais abrangente
Hero e movimento - cena 3D em tempo real com os blocos da marca (kit feito no Blender, 3d/blocos.py): marca, tela, integrações e dados; segue a rolagem, tem vida em repouso, reage ao cursor e, parada no topo, passa sozinha pelos estados com legenda - três níveis de aparelho (full, lite, static); sem WebGL fica o pôster - título novo: "Sites e sistemas sob medida para o jeito que a sua empresa trabalha." - faixa de clientes com o ramo, linha do primeiro passo, hero alinhado às seções - rótulos digitados, entrada dos blocos, cortina de blocos entre páginas e capa compartilhada entre a lista de cases e o case Ofertas - atendimento no WhatsApp com IA e nota fiscal para a reforma tributária (páginas de serviço, cartões da home, formulário e aviso com data de validade) - página /ia atualizada, com fontes - peças 3D em loop nos cartões e topos de página (3d/pecas.py) Design - hover por papel (bloco nos botões, barra nos links), rótulos sem caixa alta - um destino por rótulo: "Falar sobre meu projeto" leva ao formulário, "Chamar no WhatsApp" abre o WhatsApp - logo montado em blocos, 404 redesenhado, grades sem célula vazia - ícones do PWA e manifesto novos Specs em specs/, contexto de design em DESIGN.md e PRODUCT.md.
This commit is contained in:
1 parent
4671837c44
commit
c7d09d63d4
165 files changed
+7940
-3790
No files matched your search
+619
@@ -0,0 +1,619 @@
|
||||
"""Kit de blocos do hero: um bloco chanfrado e quatro estados com o mesmo N.
|
||||
|
||||
Rodar da raiz do repositório: blender -b --python 3d/blocos.py
|
||||
Gera public/3d/bloco.glb, public/3d/blocos.json, os dois matcaps, o pôster do hero e os dos estados (Cycles) e, em
|
||||
3d/out/, o .blend e as prévias de cada estado.
|
||||
Coordenadas do JSON: Y para cima, peça de frente para +Z, centrada na origem.
|
||||
"""
|
||||
|
||||
import ctypes
|
||||
import gzip
|
||||
import math
|
||||
import os
|
||||
import sys
|
||||
|
||||
import bmesh
|
||||
import bpy
|
||||
import numpy as np
|
||||
from bpy_extras.object_utils import world_to_camera_view
|
||||
from mathutils import Vector
|
||||
|
||||
RAIZ = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
PUBLICO = os.path.join(RAIZ, "public", "3d")
|
||||
SAIDA = os.path.join(RAIZ, "3d", "out")
|
||||
GLB = os.path.join(PUBLICO, "bloco.glb")
|
||||
JSON = os.path.join(PUBLICO, "blocos.json")
|
||||
|
||||
ESTADOS = ["marca", "tela", "integracoes", "dados"]
|
||||
PASSO = 7 # números por bloco: x, y, z, sx, sy, sz, tom
|
||||
CAIXA = (16, 11, 4)
|
||||
|
||||
# ---------------------------------------------------------------- marca
|
||||
# public/brand/mark-dark.svg (viewBox 210 220 830 800)
|
||||
POLIGONO = [
|
||||
(245, 757), (321, 757), (321, 681), (399, 681), (399, 606), (476, 606),
|
||||
(476, 757), (399, 757), (399, 878), (523, 878), (523, 757), (654, 757),
|
||||
(654, 913), (594, 913), (594, 982), (245, 982),
|
||||
]
|
||||
# a forma branca em retângulos exatos (u0, v0, u1, v1), sem sobreposição
|
||||
BRANCOS = [
|
||||
(399, 606, 476, 757),
|
||||
(321, 681, 399, 757),
|
||||
(245, 757, 399, 982),
|
||||
(523, 757, 594, 878),
|
||||
(594, 757, 654, 913),
|
||||
(399, 878, 594, 982),
|
||||
]
|
||||
LIMAS = [
|
||||
(511, 512, 667, 688),
|
||||
(667, 358, 821, 512),
|
||||
(844, 255, 947, 358),
|
||||
(906, 432, 1007, 533),
|
||||
]
|
||||
LADO_SVG = 38
|
||||
ESCALA = 10 / 727 # altura da marca (255..982) vira 10
|
||||
CU, CV = (245 + 1007) / 2, (255 + 982) / 2 # centro da marca no SVG
|
||||
|
||||
|
||||
def svg_para_mundo(u, v):
|
||||
return (u - CU) * ESCALA, -(v - CV) * ESCALA
|
||||
|
||||
|
||||
def estado_marca():
|
||||
sz = LADO_SVG * ESCALA
|
||||
blocos = []
|
||||
for tom, retangulos in ((0, BRANCOS), (1, LIMAS)):
|
||||
for u0, v0, u1, v1 in retangulos:
|
||||
nx = max(1, round((u1 - u0) / LADO_SVG))
|
||||
ny = max(1, round((v1 - v0) / LADO_SVG))
|
||||
bu, bv = (u1 - u0) / nx, (v1 - v0) / ny
|
||||
for i in range(nx):
|
||||
for j in range(ny):
|
||||
x, y = svg_para_mundo(u0 + (i + 0.5) * bu, v0 + (j + 0.5) * bv)
|
||||
for z in (-sz / 2, sz / 2):
|
||||
blocos.append((x, y, z, bu * ESCALA, bv * ESCALA, sz, tom))
|
||||
return blocos
|
||||
|
||||
|
||||
# ------------------------------------------- tela, integrações, dados
|
||||
# Desenhados numa grade inteira (coluna i, linha j com j = 0 no topo, camada k
|
||||
# para +Z). O passo em mundo sai do tamanho da grade; a densidade de cada
|
||||
# desenho foi escolhida para ficar entre 85% e 100% de N.
|
||||
|
||||
|
||||
class Grade:
|
||||
def __init__(self):
|
||||
self.celulas = {}
|
||||
|
||||
def bloco(self, i, j, k=0, tom=0):
|
||||
# lima prevalece quando dois elementos caem na mesma célula
|
||||
chave = (round(i), round(j), k)
|
||||
self.celulas[chave] = max(tom, self.celulas.get(chave, 0))
|
||||
|
||||
def cheio(self, i0, j0, i1, j1, k=0, tom=0):
|
||||
for i in range(i0, i1 + 1):
|
||||
for j in range(j0, j1 + 1):
|
||||
self.bloco(i, j, k, tom)
|
||||
|
||||
def contorno(self, i0, j0, i1, j1, k=0, tom=0):
|
||||
for i in range(i0, i1 + 1):
|
||||
self.bloco(i, j0, k, tom)
|
||||
self.bloco(i, j1, k, tom)
|
||||
for j in range(j0 + 1, j1):
|
||||
self.bloco(i0, j, k, tom)
|
||||
self.bloco(i1, j, k, tom)
|
||||
|
||||
def linha(self, a, b, k=0, tom=0):
|
||||
n = math.ceil(max(abs(b[0] - a[0]), abs(b[1] - a[1]))) * 2 + 1
|
||||
for t in range(n + 1):
|
||||
f = t / n
|
||||
self.bloco(
|
||||
math.floor(a[0] + (b[0] - a[0]) * f + 0.5),
|
||||
math.floor(a[1] + (b[1] - a[1]) * f + 0.5),
|
||||
k,
|
||||
tom,
|
||||
)
|
||||
|
||||
def disco(self, ci, cj, r, k=0, tom=0):
|
||||
n = math.floor(r)
|
||||
for di in range(-n, n + 1):
|
||||
for dj in range(-n, n + 1):
|
||||
if di * di + dj * dj <= r * r:
|
||||
self.bloco(ci + di, cj + dj, k, tom)
|
||||
|
||||
def blocos(self):
|
||||
cel = self.celulas
|
||||
ii = [c[0] for c in cel]
|
||||
jj = [c[1] for c in cel]
|
||||
kk = [c[2] for c in cel]
|
||||
largura = max(ii) - min(ii) + 1
|
||||
altura = max(jj) - min(jj) + 1
|
||||
camadas = max(kk) - min(kk) + 1
|
||||
# maior passo que cabe na caixa, com folga
|
||||
p = min(14.5 / largura, 9.8 / altura, 3.8 / camadas)
|
||||
mi, mj, mk = (max(ii) + min(ii)) / 2, (max(jj) + min(jj)) / 2, (max(kk) + min(kk)) / 2
|
||||
return [
|
||||
((i - mi) * p, -(j - mj) * p, (k - mk) * p, p, p, p, tom)
|
||||
for (i, j, k), tom in sorted(cel.items())
|
||||
]
|
||||
|
||||
|
||||
def estado_tela():
|
||||
# referência: drawScreen em app/components/cinematic/HeroFieldScene.tsx
|
||||
g = Grade()
|
||||
C, R = 26, 18
|
||||
g.contorno(0, 0, C - 1, R - 1) # moldura
|
||||
for i in (2, 4, 6):
|
||||
g.bloco(i, 1) # três pontos
|
||||
g.cheio(1, 2, C - 2, 2) # barra do navegador
|
||||
g.cheio(2, 4, 9, 5) # título
|
||||
g.cheio(2, 7, 12, 7) # texto
|
||||
g.cheio(2, 9, 9, 9)
|
||||
for k in (0, 1):
|
||||
g.cheio(2, 11, 7, 11, k, tom=1) # botão, saltado para a frente
|
||||
g.contorno(15, 4, C - 3, 11) # imagem
|
||||
g.linha((16, 10), (19, 7))
|
||||
g.linha((19, 7), (21, 9))
|
||||
g.bloco(21, 6)
|
||||
for i0 in (2, 10, 18):
|
||||
g.contorno(i0, 13, i0 + 5, 15) # cartões
|
||||
return g.blocos()
|
||||
|
||||
|
||||
def estado_integracoes():
|
||||
# referência: drawIntegrations em HeroFieldScene.tsx
|
||||
g = Grade()
|
||||
A, B = 11, 7.5 # semieixos da elipse dos nós
|
||||
r_nucleo, r_no = 2.6, 1.6
|
||||
for k in (0, 1):
|
||||
g.disco(0, 0, r_nucleo, k, tom=1)
|
||||
nos = []
|
||||
for n in range(6):
|
||||
a = n / 6 * math.tau + math.pi / 6
|
||||
nos.append((round(math.cos(a) * A), round(math.sin(a) * B)))
|
||||
for n, (x, y) in enumerate(nos):
|
||||
for k in (0, 1):
|
||||
g.disco(x, y, r_no, k)
|
||||
g.bloco(x, y, 1, tom=1)
|
||||
d = math.hypot(x, y)
|
||||
ux, uy = x / d, y / d
|
||||
g.linha((ux * (r_nucleo + 0.5), uy * (r_nucleo + 0.5)), (x - ux * (r_no + 0.5), y - uy * (r_no + 0.5)))
|
||||
nx, ny = nos[(n + 1) % 6]
|
||||
dx, dy = nx - x, ny - y
|
||||
d = math.hypot(dx, dy)
|
||||
g.linha((x + dx / d * (r_no + 0.5), y + dy / d * (r_no + 0.5)), (nx - dx / d * (r_no + 0.5), ny - dy / d * (r_no + 0.5)))
|
||||
return g.blocos()
|
||||
|
||||
|
||||
def estado_dados():
|
||||
# referência: drawData em HeroFieldScene.tsx; a coluna da direita vira barras em +Z
|
||||
g = Grade()
|
||||
C, R = 24, 15
|
||||
g.contorno(0, 0, C - 1, R - 1)
|
||||
g.cheio(1, 1, C - 2, 2, tom=1) # cabeçalho
|
||||
colunas = (7, 13, 18)
|
||||
for c in colunas:
|
||||
g.cheio(c, 3, c, R - 2)
|
||||
valores = ((5, 3, 2), (3, 4, 3), (4, 2, 1), (2, 4, 3), (5, 3, 2))
|
||||
alturas = (2, 4, 1, 5, 3)
|
||||
for linha, (v, h) in enumerate(zip(valores, alturas)):
|
||||
j = 4 + linha * 2
|
||||
for inicio, comprimento in zip((2, 9, 15), v):
|
||||
g.cheio(inicio, j, inicio + comprimento - 1, j)
|
||||
for k in range(h + 1):
|
||||
g.cheio(20, j, 21, j, k, tom=int(k == h)) # topo da barra em lima
|
||||
return g.blocos()
|
||||
|
||||
|
||||
# ------------------------------------------------ montagem dos estados
|
||||
|
||||
|
||||
def chave(b):
|
||||
return (round(b[0], 3), round(b[1], 3), round(b[2], 3))
|
||||
|
||||
|
||||
def completar(blocos, n):
|
||||
"""Ordena por (x, y, z) e põe os que faltam para n ocultos sobre visíveis espalhados."""
|
||||
visiveis = sorted(blocos, key=chave)
|
||||
v, h = len(visiveis), n - len(visiveis)
|
||||
if h < 0:
|
||||
falhar(f"estado com {v} blocos, mais que N={n}")
|
||||
ocultos = []
|
||||
for m in range(h):
|
||||
x, y, z, _, _, _, tom = visiveis[(m * 2 + 1) * v // (h * 2)]
|
||||
ocultos.append((x, y, z, 0, 0, 0, tom))
|
||||
# oculto fica logo depois do visível em que está, mesma chave espacial
|
||||
return sorted(visiveis + ocultos, key=lambda b: (chave(b), b[3] == 0))
|
||||
|
||||
|
||||
def fmt(v):
|
||||
s = f"{v:.3f}"
|
||||
return "0.000" if s == "-0.000" else s
|
||||
|
||||
|
||||
# ------------------------------------------------------------- Blender
|
||||
|
||||
|
||||
def srgb(h):
|
||||
def lin(c):
|
||||
c /= 255
|
||||
return c / 12.92 if c <= 0.04045 else ((c + 0.055) / 1.055) ** 2.4
|
||||
|
||||
return (*(lin(int(h[i : i + 2], 16)) for i in (0, 2, 4)), 1.0)
|
||||
|
||||
|
||||
def criar_bloco():
|
||||
malha = bpy.data.meshes.new("bloco")
|
||||
bm = bmesh.new()
|
||||
bmesh.ops.create_cube(bm, size=1)
|
||||
bm.to_mesh(malha)
|
||||
bm.free()
|
||||
obj = bpy.data.objects.new("bloco", malha)
|
||||
bpy.context.scene.collection.objects.link(obj)
|
||||
bpy.context.view_layer.objects.active = obj
|
||||
obj.select_set(True)
|
||||
chanfro = obj.modifiers.new("chanfro", "BEVEL")
|
||||
chanfro.width = 0.08
|
||||
chanfro.segments = 2
|
||||
malha.shade_smooth()
|
||||
pesos = obj.modifiers.new("normais", "WEIGHTED_NORMAL")
|
||||
pesos.keep_sharp = True
|
||||
for m in ("chanfro", "normais"):
|
||||
bpy.ops.object.modifier_apply(modifier=m)
|
||||
return obj
|
||||
|
||||
|
||||
def exportar_glb(obj):
|
||||
bpy.ops.object.select_all(action="DESELECT")
|
||||
obj.select_set(True)
|
||||
bpy.ops.export_scene.gltf(
|
||||
filepath=GLB,
|
||||
export_format="GLB",
|
||||
use_selection=True,
|
||||
export_yup=True,
|
||||
export_apply=True,
|
||||
export_normals=True,
|
||||
export_texcoords=False,
|
||||
export_tangents=False,
|
||||
export_materials="NONE",
|
||||
export_vertex_color="NONE",
|
||||
export_attributes=False,
|
||||
export_extras=False,
|
||||
export_animations=False,
|
||||
export_draco_mesh_compression_enable=False,
|
||||
)
|
||||
|
||||
|
||||
def montar_cena(estados, malha):
|
||||
cena = bpy.context.scene
|
||||
materiais = criar_materiais()
|
||||
malha.materials.append(materiais[0])
|
||||
colecoes = {}
|
||||
for nome, blocos in zip(ESTADOS, estados):
|
||||
col = bpy.data.collections.new(nome)
|
||||
cena.collection.children.link(col)
|
||||
colecoes[nome] = col
|
||||
for n, (x, y, z, sx, sy, sz, tom) in enumerate(blocos):
|
||||
obj = bpy.data.objects.new(f"{nome}.{n:03d}", malha)
|
||||
col.objects.link(obj)
|
||||
# arquivo Y-up/+Z frente -> Blender Z-up/-Y frente
|
||||
obj.location = (x, -z, y)
|
||||
obj.scale = (sx, sz, sy)
|
||||
obj.material_slots[0].link = "OBJECT"
|
||||
obj.material_slots[0].material = materiais[tom]
|
||||
obj.hide_render = obj.hide_viewport = sx == 0
|
||||
return colecoes, materiais
|
||||
|
||||
|
||||
def renderizar(colecoes):
|
||||
cena = bpy.context.scene
|
||||
cena.render.engine = "BLENDER_WORKBENCH"
|
||||
cena.render.resolution_x, cena.render.resolution_y = 800, 600
|
||||
cena.render.resolution_percentage = 100
|
||||
cena.render.image_settings.file_format = "PNG"
|
||||
cena.view_settings.view_transform = "Standard"
|
||||
cena.display.shading.light = "STUDIO"
|
||||
cena.display.shading.color_type = "MATERIAL"
|
||||
cena.display.shading.show_cavity = True
|
||||
cena.world = bpy.data.worlds.new("fundo")
|
||||
cena.world.color = (0.008, 0.008, 0.01)
|
||||
|
||||
cam = bpy.data.objects.new("camera", bpy.data.cameras.new("camera"))
|
||||
cena.collection.objects.link(cam)
|
||||
cam.location = (-6.0, -25.0, 3.0) # pela esquerda: barras de dados saem para fora da tabela
|
||||
cam.rotation_euler = (-cam.location.normalized()).to_track_quat("-Z", "Y").to_euler()
|
||||
cena.camera = cam
|
||||
|
||||
for nome in ESTADOS:
|
||||
for outro, col in colecoes.items():
|
||||
col.hide_render = col.hide_viewport = outro != nome
|
||||
cena.render.filepath = os.path.join(SAIDA, f"previa-{nome}.png")
|
||||
bpy.ops.render.render(write_still=True)
|
||||
for outro, col in colecoes.items():
|
||||
col.hide_render = col.hide_viewport = outro != "marca"
|
||||
|
||||
|
||||
# ------------------------------------------------------- render final
|
||||
# Porcelana fosca com um verniz fino e esmalte lima com emissão leve, numa luz de estúdio de três pontos.
|
||||
# Os matcaps (uma esfera de cada material, vista de frente) levam essa mesma luz para o tempo real.
|
||||
|
||||
|
||||
def criar_materiais():
|
||||
def material(nome, cor, **entradas):
|
||||
mat = bpy.data.materials.new(nome)
|
||||
mat.diffuse_color = srgb(cor) # cor da prévia (Workbench)
|
||||
bsdf = mat.node_tree.nodes["Principled BSDF"]
|
||||
bsdf.inputs["Base Color"].default_value = srgb(cor)
|
||||
for entrada, valor in entradas.items():
|
||||
bsdf.inputs[entrada].default_value = valor
|
||||
return mat
|
||||
|
||||
return {
|
||||
0: material("porcelana", "f4f3ef", **{"Roughness": 0.5, "Coat Weight": 0.12, "Coat Roughness": 0.3}),
|
||||
1: material(
|
||||
"lima",
|
||||
"d5fa00",
|
||||
**{
|
||||
"Roughness": 0.28,
|
||||
"Coat Weight": 0.3,
|
||||
"Coat Roughness": 0.12,
|
||||
"Emission Color": srgb("d5fa00"),
|
||||
"Emission Strength": 0.06,
|
||||
},
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def olhar_para_a_origem(obj):
|
||||
obj.rotation_euler = (-Vector(obj.location).normalized()).to_track_quat("-Z", "Y").to_euler()
|
||||
|
||||
|
||||
def enquadrar(camera, objetos, direcao, ocupa):
|
||||
"""Põe a câmera na direção dada, olhando a origem, na distância em que os objetos ocupam essa fração do quadro."""
|
||||
cena = bpy.context.scene
|
||||
cantos = [obj.matrix_world @ Vector(c) for obj in objetos for c in obj.bound_box]
|
||||
distancia = 40
|
||||
for _ in range(3): # com lente longa o tamanho na tela é quase 1 / distância: três passos convergem
|
||||
camera.location = Vector(direcao).normalized() * distancia
|
||||
olhar_para_a_origem(camera)
|
||||
bpy.context.view_layer.update()
|
||||
vistos = [world_to_camera_view(cena, camera, p) for p in cantos]
|
||||
distancia *= 2 * max(max(abs(p.x - 0.5), abs(p.y - 0.5)) for p in vistos) / ocupa
|
||||
|
||||
|
||||
def luz(nome, posicao, lado, potencia, cor=(1, 1, 1)):
|
||||
dados = bpy.data.lights.new(nome, "AREA")
|
||||
dados.shape = "SQUARE"
|
||||
dados.size = lado
|
||||
dados.energy = potencia
|
||||
dados.color = cor
|
||||
obj = bpy.data.objects.new(nome, dados)
|
||||
bpy.context.scene.collection.objects.link(obj)
|
||||
obj.location = posicao
|
||||
olhar_para_a_origem(obj)
|
||||
|
||||
|
||||
def ligar_cycles(cena):
|
||||
"""Cycles na GPU com OptiX quando a máquina tem; senão segue na CPU."""
|
||||
cena.render.engine = "CYCLES"
|
||||
cena.cycles.use_denoising = True
|
||||
prefs = bpy.context.preferences.addons["cycles"].preferences
|
||||
try:
|
||||
prefs.compute_device_type = "OPTIX"
|
||||
except TypeError:
|
||||
return
|
||||
prefs.get_devices_for_type("OPTIX")
|
||||
for aparelho in prefs.devices:
|
||||
aparelho.use = aparelho.type == "OPTIX"
|
||||
if any(aparelho.use for aparelho in prefs.devices):
|
||||
cena.cycles.device = "GPU"
|
||||
cena.cycles.denoiser = "OPTIX"
|
||||
|
||||
|
||||
def sair(cena, caminho, lado, formato, modo, amostras, transparente):
|
||||
cena.render.resolution_x, cena.render.resolution_y = lado
|
||||
cena.render.film_transparent = transparente
|
||||
cena.render.image_settings.file_format = formato
|
||||
cena.render.image_settings.color_mode = modo
|
||||
cena.render.image_settings.quality = 84
|
||||
cena.cycles.samples = amostras
|
||||
cena.render.filepath = caminho
|
||||
bpy.ops.render.render(write_still=True)
|
||||
|
||||
|
||||
def luz_de_estudio(cena):
|
||||
fundo = cena.world.node_tree.nodes["Background"]
|
||||
fundo.inputs["Color"].default_value = (0.02, 0.022, 0.02, 1)
|
||||
fundo.inputs["Strength"].default_value = 1.0
|
||||
# três tons por bloco: topo claro (principal, quase a pino), frente média (preenchimento baixo e frontal) e a lateral
|
||||
# que a câmera vê na sombra; o contraluz recorta a silhueta por trás
|
||||
luz("principal", (6, -10, 20), 14, 7000)
|
||||
luz("preenchimento", (-10, -18, 3), 18, 2000, (0.92, 0.96, 1.0))
|
||||
luz("contraluz", (-8, 12, 10), 10, 2000, (0.85, 1.0, 0.75))
|
||||
|
||||
|
||||
def render_final(colecoes, materiais):
|
||||
cena = bpy.context.scene
|
||||
ligar_cycles(cena)
|
||||
luz_de_estudio(cena)
|
||||
|
||||
def so(nome):
|
||||
for outro, col in colecoes.items():
|
||||
col.hide_render = col.hide_viewport = outro != nome
|
||||
|
||||
# prévias finais dos quatro estados, na câmera das prévias rápidas
|
||||
for nome in ESTADOS:
|
||||
so(nome)
|
||||
sair(cena, os.path.join(SAIDA, f"final-{nome}.png"), (800, 600), "PNG", "RGB", 64, False)
|
||||
|
||||
camera = bpy.data.objects.new("camera-poster", bpy.data.cameras.new("camera-poster"))
|
||||
cena.collection.objects.link(camera)
|
||||
camera.data.lens = 85
|
||||
cena.camera = camera
|
||||
|
||||
# pôsteres dos estados: quase de frente como as prévias, mas de lente longa e com a peça em uns 85% do quadro
|
||||
cena.render.resolution_x, cena.render.resolution_y = 1200, 900 # o enquadramento usa a proporção do quadro
|
||||
for nome in ESTADOS[1:]:
|
||||
so(nome)
|
||||
enquadrar(camera, [obj for obj in colecoes[nome].objects if not obj.hide_render], (-6, -25, 3), 0.85)
|
||||
sair(cena, os.path.join(PUBLICO, f"estado-{nome}.webp"), (1200, 900), "WEBP", "RGBA", 256, True)
|
||||
|
||||
# pôster: a marca em três quartos, fundo transparente
|
||||
camera.location = Vector((-0.42, -0.86, 0.2)).normalized() * 36
|
||||
olhar_para_a_origem(camera)
|
||||
so("marca")
|
||||
sair(cena, os.path.join(PUBLICO, "hero-poster.webp"), (1200, 1200), "WEBP", "RGBA", 256, True)
|
||||
|
||||
# matcaps: uma esfera de cada material, de frente, em vista ortográfica
|
||||
so(None)
|
||||
malha = bpy.data.meshes.new("esfera")
|
||||
bm = bmesh.new()
|
||||
bmesh.ops.create_uvsphere(bm, u_segments=96, v_segments=48, radius=1)
|
||||
bm.to_mesh(malha)
|
||||
bm.free()
|
||||
malha.shade_smooth()
|
||||
esfera = bpy.data.objects.new("esfera", malha)
|
||||
cena.collection.objects.link(esfera)
|
||||
frente = bpy.data.objects.new("camera-matcap", bpy.data.cameras.new("camera-matcap"))
|
||||
cena.collection.objects.link(frente)
|
||||
frente.data.type = "ORTHO"
|
||||
frente.data.ortho_scale = 2.0
|
||||
frente.location = (0, -12, 0)
|
||||
olhar_para_a_origem(frente)
|
||||
cena.camera = frente
|
||||
malha.materials.append(None)
|
||||
# a esfera de frente pega mais luz de preenchimento que as faces do pôster: a porcelana sai 0,3 ponto de exposição
|
||||
# abaixo, para a cena em tempo real ter o mesmo branco do pôster (a lima já bate)
|
||||
for tom, nome, exposicao in ((0, "porcelana", -0.3), (1, "lima", 0.0)):
|
||||
malha.materials[0] = materiais[tom]
|
||||
cena.view_settings.exposure = exposicao
|
||||
sair(cena, os.path.join(PUBLICO, f"matcap-{nome}.webp"), (256, 256), "WEBP", "RGB", 128, False)
|
||||
cena.view_settings.exposure = 0.0
|
||||
bpy.data.objects.remove(esfera)
|
||||
bpy.data.objects.remove(frente)
|
||||
|
||||
cena.camera = camera
|
||||
so("marca")
|
||||
|
||||
|
||||
# ------------------------------------------------------- autoconferência
|
||||
|
||||
|
||||
def falhar(msg):
|
||||
print(f"FALHOU: {msg}", flush=True)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def conferir(tris):
|
||||
with open(JSON, "rb") as f:
|
||||
bruto = f.read()
|
||||
import json
|
||||
|
||||
doc = json.loads(bruto)
|
||||
n = doc["n"]
|
||||
dados = np.array(doc["dados"], dtype=float)
|
||||
if dados.size != len(ESTADOS) * n * PASSO:
|
||||
falhar(f"dados com {dados.size} números, esperado {len(ESTADOS) * n * PASSO} (mesmo N em todos os estados)")
|
||||
dados = dados.reshape(len(ESTADOS), n, PASSO)
|
||||
if n > 320:
|
||||
falhar(f"N={n} > 320")
|
||||
if n * tris > 60000:
|
||||
falhar(f"N x triângulos = {n * tris} > 60000")
|
||||
if os.path.getsize(GLB) > 40 * 1024:
|
||||
falhar(f"bloco.glb com {os.path.getsize(GLB)} bytes > 40 KB")
|
||||
estados = [(f"estado-{nome}.webp", 90) for nome in ESTADOS[1:]]
|
||||
for nome, limite in (("hero-poster.webp", 90), *estados, ("matcap-porcelana.webp", 30), ("matcap-lima.webp", 30)):
|
||||
tamanho = os.path.getsize(os.path.join(PUBLICO, nome))
|
||||
if tamanho > limite * 1024:
|
||||
falhar(f"{nome} com {tamanho} bytes > {limite} KB")
|
||||
if len(gzip.compress(bruto)) > 40 * 1024:
|
||||
falhar("blocos.json em gzip > 40 KB")
|
||||
|
||||
meia = np.array(CAIXA) / 2 + 1e-3
|
||||
for nome, e in zip(ESTADOS, dados):
|
||||
vis = e[e[:, 3] > 0]
|
||||
ocultos = n - len(vis)
|
||||
if ocultos > 0.15 * n:
|
||||
falhar(f"{nome}: {ocultos} ocultos > 15% de {n}")
|
||||
if np.any(np.abs(vis[:, :3]) + vis[:, 3:6] / 2 > meia):
|
||||
falhar(f"{nome}: sai da caixa {CAIXA}")
|
||||
if nome != "marca":
|
||||
if np.any(np.abs(vis[:, 3] - vis[:, 4]) > 1e-3) or np.any(np.abs(vis[:, 3] - vis[:, 5]) > 1e-3):
|
||||
falhar(f"{nome}: blocos não cúbicos")
|
||||
if vis[:, 6].sum() > 0.25 * len(vis):
|
||||
falhar(f"{nome}: lima em {int(vis[:, 6].sum())} de {len(vis)} visíveis > 25%")
|
||||
|
||||
# marca: em cada camada, os blocos lima ladrilham os quatro retângulos com tolerância
|
||||
# de 0.5 unidade do SVG: cada bloco dentro de um retângulo, sem sobreposição e com a
|
||||
# área de cada retângulo preenchida (folga de 0.5 em todo o perímetro).
|
||||
tol = 0.5
|
||||
marca = dados[0]
|
||||
area_poligono = abs(sum(
|
||||
a[0] * b[1] - b[0] * a[1] for a, b in zip(POLIGONO, POLIGONO[1:] + POLIGONO[:1])
|
||||
)) / 2
|
||||
for z in np.unique(marca[:, 2]):
|
||||
camada = marca[marca[:, 2] == z]
|
||||
u = camada[:, 0] / ESCALA + CU
|
||||
v = -camada[:, 1] / ESCALA + CV
|
||||
lu = camada[:, 3] / ESCALA
|
||||
lv = camada[:, 4] / ESCALA
|
||||
caixas = [(u[b] - lu[b] / 2, v[b] - lv[b] / 2, u[b] + lu[b] / 2, v[b] + lv[b] / 2)
|
||||
for b in np.flatnonzero(camada[:, 6] == 1)]
|
||||
areas = [0.0] * len(LIMAS)
|
||||
for c in caixas:
|
||||
dentro = [r for r, (u0, v0, u1, v1) in enumerate(LIMAS)
|
||||
if c[0] >= u0 - tol and c[1] >= v0 - tol and c[2] <= u1 + tol and c[3] <= v1 + tol]
|
||||
if not dentro:
|
||||
falhar(f"marca z={z}: bloco lima {c} fora dos retângulos lima")
|
||||
areas[dentro[0]] += (c[2] - c[0]) * (c[3] - c[1])
|
||||
for (u0, v0, u1, v1), area in zip(LIMAS, areas):
|
||||
if abs(area - (u1 - u0) * (v1 - v0)) > tol * 2 * (u1 - u0 + v1 - v0):
|
||||
falhar(f"marca z={z}: retângulo lima {(u0, v0, u1, v1)} coberto com área {area:.0f}")
|
||||
for a in range(len(caixas)):
|
||||
for b in range(a):
|
||||
p, q = caixas[a], caixas[b]
|
||||
if min(p[2], q[2]) - max(p[0], q[0]) > tol and min(p[3], q[3]) - max(p[1], q[1]) > tol:
|
||||
falhar(f"marca z={z}: blocos lima sobrepostos {p} {q}")
|
||||
brancos = camada[:, 6] == 0
|
||||
area = float((lu[brancos] * lv[brancos]).sum())
|
||||
if abs(area - area_poligono) > 0.01 * area_poligono:
|
||||
falhar(f"marca z={z}: área branca {area:.0f} != polígono {area_poligono:.0f}")
|
||||
|
||||
|
||||
def main():
|
||||
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||||
os.makedirs(PUBLICO, exist_ok=True)
|
||||
os.makedirs(SAIDA, exist_ok=True)
|
||||
|
||||
marca = estado_marca()
|
||||
n = len(marca)
|
||||
estados = [completar(b, n) for b in (marca, estado_tela(), estado_integracoes(), estado_dados())]
|
||||
numeros = ",".join(fmt(v) for e in estados for b in e for v in b)
|
||||
lista = ",".join(f'"{e}"' for e in ESTADOS)
|
||||
with open(JSON, "w") as f:
|
||||
f.write(f'{{"n":{n},"passo":{PASSO},"estados":[{lista}],"dados":[{numeros}]}}')
|
||||
|
||||
bloco = criar_bloco()
|
||||
bloco.data.calc_loop_triangles()
|
||||
tris = len(bloco.data.loop_triangles)
|
||||
exportar_glb(bloco)
|
||||
|
||||
bpy.context.scene.collection.objects.unlink(bloco) # a malha fica só nas instâncias
|
||||
colecoes, materiais = montar_cena(estados, bloco.data)
|
||||
bpy.data.objects.remove(bloco)
|
||||
renderizar(colecoes)
|
||||
render_final(colecoes, materiais)
|
||||
bpy.context.preferences.filepaths.save_version = 0 # sem blocos.blend1
|
||||
bpy.ops.wm.save_as_mainfile(filepath=os.path.join(SAIDA, "blocos.blend"))
|
||||
|
||||
conferir(tris)
|
||||
for nome, e in zip(ESTADOS, estados):
|
||||
vis = [b for b in e if b[3] > 0]
|
||||
print(f"{nome}: {len(vis)} visíveis, {sum(b[6] for b in vis)} lima", flush=True)
|
||||
ctypes.CDLL(None).fflush(None) # despeja o buffer do C (banner do Blender) antes do OK
|
||||
print(f"OK blocos={n} tris={n * tris}", flush=True)
|
||||
sys.exit(0) # sai já, para o OK ser a última linha
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
+341
@@ -0,0 +1,341 @@
|
||||
"""Peças pré-renderizadas dos cartões e do 404: o kit de blocos.py animado em loop de 3 s.
|
||||
|
||||
Rodar da raiz do repositório: blender -b --python 3d/pecas.py
|
||||
Renderiza os quadros em 3d/out/pecas/<nome>/ e o ffmpeg do sistema gera em public/3d/
|
||||
peca-<nome>.webm (VP9), .mp4 (H.264) e .webp (pôster). Mesmos bloco, materiais e luz do pôster do hero.
|
||||
"""
|
||||
|
||||
import ctypes
|
||||
import math
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
import bpy
|
||||
from mathutils import Matrix, Vector
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
from blocos import ( # noqa: E402
|
||||
LIMAS,
|
||||
PUBLICO,
|
||||
SAIDA,
|
||||
criar_bloco,
|
||||
criar_materiais,
|
||||
estado_marca,
|
||||
falhar,
|
||||
ligar_cycles,
|
||||
luz_de_estudio,
|
||||
sair,
|
||||
svg_para_mundo,
|
||||
)
|
||||
|
||||
QUADROS = 72 # 3 s a 24 por segundo; o quadro 72 seria de novo o 0
|
||||
LADO = (640, 400)
|
||||
TINTA = "0x0a0a0a" # --ink
|
||||
AMOSTRAS = 64
|
||||
LIMITE_VIDEO, LIMITE_POSTER = 250_000, 40_000
|
||||
|
||||
|
||||
def encaixe(f, inicio, duracao):
|
||||
"""0 antes, 1 assentado: chega rápido e assenta com ease-out cúbico, sem passar do ponto."""
|
||||
t = min(max((f - inicio) / duracao, 0), 1)
|
||||
return 1 - (1 - t) ** 3
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- peças
|
||||
# Cada peça recebe novo(posição, escala, tom) e devolve pose(f), que põe os blocos no quadro f.
|
||||
# Coordenadas do Blender: X para a direita, -Y para a câmera, Z para cima; bloco de lado 1.
|
||||
|
||||
|
||||
def sistema(novo):
|
||||
# planilha deitada de 7 x 5; a linha do fundo, em lima, é o cabeçalho
|
||||
for c in range(7):
|
||||
for r in range(5):
|
||||
novo((c - 3, 2 - r, 0), 1, int(r == 0))
|
||||
# as barras moram dentro das células das colunas 1 a 5, logo à frente do cabeçalho, e sobem telescópicas
|
||||
barras = [[novo((k - 2, 1, 0), 0.98, int(j == h - 1)) for j in range(h)] for k, h in enumerate((2, 3, 5, 4, 6))]
|
||||
|
||||
def pose(f):
|
||||
for k, pilha in enumerate(barras):
|
||||
sobe = encaixe(f, 6 + 4 * k, 8) - encaixe(f, 42 + 4 * k, 8)
|
||||
for j, obj in enumerate(pilha):
|
||||
obj.location.z = (1 + j) * sobe
|
||||
|
||||
return pose
|
||||
|
||||
|
||||
def whatsapp(novo):
|
||||
# celular em pé de 9 x 16 (x = c - 4, z = 7.5 - r), de cantos arredondados; o miolo vazio é a tela
|
||||
for c in range(9):
|
||||
for r in range(16):
|
||||
borda = (c in (0, 8) or r in (0, 15)) and not (c in (0, 8) and r in (0, 15))
|
||||
canto_de_dentro = c in (1, 7) and r in (1, 14)
|
||||
if borda or canto_de_dentro or r == 2: # r = 2 fecha a barra de cima
|
||||
novo((c - 4, 0, 7.5 - r), 1, 0)
|
||||
novo((-2, 0, 6.5), 1, 1) # foto do contato
|
||||
for i in range(4):
|
||||
novo((-0.8 + 0.7 * i, 0, 6.5), 0.7, 0) # nome, nos blocos menores dos balões, para ler como texto
|
||||
|
||||
# balões de 6 x 3 blocos menores, alternando os lados; o canto de baixo do lado do balão fica reto (é o rabo)
|
||||
p = 0.7
|
||||
|
||||
def balao(k, esquerda, tom):
|
||||
borda = -3.15 if esquerda else 3.15 # parede da tela menos meio bloco de folga
|
||||
sentido = 1 if esquerda else -1
|
||||
topo = 2.95 - 3.15 * k
|
||||
blocos = []
|
||||
for i in range(6):
|
||||
for j in range(3):
|
||||
if i in (0, 5) and j in (0, 2) and (i, j) != (0, 2):
|
||||
continue
|
||||
alvo = Vector((borda + sentido * (i + 0.5) * p, 0, topo - j * p))
|
||||
blocos.append((novo(alvo, p, tom), alvo))
|
||||
return blocos, Vector((borda, 0, topo - 2.5 * p))
|
||||
|
||||
baloes = [balao(0, True, 0), balao(1, False, 1), balao(2, True, 0)]
|
||||
pontos = [novo((-2.25 + d, 0, 2.95 - 6.3 - p), 0.55, 0) for d in range(3)] # "digitando", no lugar do terceiro
|
||||
|
||||
def pose(f):
|
||||
some = min(max((f - 60) / 8, 0), 1) ** 3 # somem juntos, encolhendo para o rabo
|
||||
for (blocos, rabo), chega in zip(baloes, (2, 14, 44)):
|
||||
s = encaixe(f, chega, 8) * (1 - some)
|
||||
for obj, alvo in blocos:
|
||||
obj.location = rabo + (alvo - rabo) * s
|
||||
obj.scale = (p * max(s, 0.001),) * 3
|
||||
obj.hide_render = s <= 0
|
||||
aparece = encaixe(f, 26, 4) * (1 - encaixe(f, 41, 3))
|
||||
for d, obj in enumerate(pontos):
|
||||
pulso = max(0.0, math.sin(math.tau * (f - 28) / 8 - d * math.tau / 6)) # um depois do outro
|
||||
s = aparece * 0.55 * (1 + 0.45 * pulso)
|
||||
obj.scale = (max(s, 0.001),) * 3
|
||||
obj.hide_render = s <= 0
|
||||
|
||||
return pose
|
||||
|
||||
|
||||
def quatrocentos(novo):
|
||||
# o quadrado lima mais alto se solta para a frente (passa diante do outro) e cai; volta de cima direto no lugar
|
||||
x0, y1 = svg_para_mundo(*LIMAS[2][:2])
|
||||
x1, y0 = svg_para_mundo(*LIMAS[2][2:])
|
||||
solto = []
|
||||
for x, y, z, sx, sy, sz, tom in estado_marca():
|
||||
obj = novo((x, -z, y), (sx, sz, sy), tom)
|
||||
if tom and x0 < x < x1 and y0 < y < y1:
|
||||
solto.append((obj, Vector((x, -z, y))))
|
||||
centro = sum((alvo for _, alvo in solto), Vector()) / len(solto)
|
||||
|
||||
def pose(f):
|
||||
if f < 32:
|
||||
queda = min(max((f - 12) / 12, 0), 1) ** 2 # ease-in: a gravidade acelera
|
||||
d = Vector((0, -1.4 * encaixe(f, 8, 4), -16 * queda))
|
||||
giro = Matrix.Rotation(0.6 * queda, 3, "Y") # tomba de lado enquanto cai
|
||||
else:
|
||||
d = Vector((0, 0, 12 * (1 - encaixe(f, 40, 8))))
|
||||
giro = Matrix.Identity(3)
|
||||
for obj, alvo in solto:
|
||||
obj.location = centro + giro @ (alvo - centro) + d
|
||||
obj.rotation_euler = giro.to_euler()
|
||||
|
||||
return pose
|
||||
|
||||
|
||||
def selo_conferido(novo, x0, z0, y, passo):
|
||||
"""Selo lima de 7 x 7 com o V de conferido vazado; (x0, z0) é o centro do bloco de cima à esquerda."""
|
||||
desenho = ("....#", "...##", "#.##.", "###..", ".#...")
|
||||
vazado = {(1 + i, 1 + j) for j, linha in enumerate(desenho) for i, x in enumerate(linha) if x == "#"}
|
||||
blocos = []
|
||||
for c in range(7):
|
||||
for r in range(7):
|
||||
if (c, r) not in vazado:
|
||||
alvo = Vector((x0 + c * passo, y, z0 - r * passo))
|
||||
blocos.append((novo(alvo, passo, 1), alvo))
|
||||
return blocos
|
||||
|
||||
|
||||
def fiscal(novo):
|
||||
# nota em pé de 11 x 16; as quatro linhas de texto são vãos na folha, a partir da coluna 1
|
||||
linhas = {2: 7, 4: 9, 6: 6, 8: 8}
|
||||
for c in range(11):
|
||||
for r in range(16):
|
||||
if not 1 <= c <= linhas.get(r, 0):
|
||||
novo((c - 5, 0, 7.5 - r), 1, 0)
|
||||
selo = selo_conferido(novo, -1, -1.5, -1, 1) # à frente do canto de baixo à direita; a folha aparece pelo V
|
||||
de_fora = Vector((22, -6, 4)) # chega pela direita, vindo da frente
|
||||
para_cima = Vector((6, -4, 20))
|
||||
|
||||
def pose(f):
|
||||
sai = min(max((f - 46) / 10, 0), 1) ** 3 # solta devagar e sai acelerando
|
||||
d = de_fora * (1 - encaixe(f, 6, 10)) + para_cima * sai
|
||||
for obj, alvo in selo:
|
||||
obj.location = alvo + d
|
||||
|
||||
return pose
|
||||
|
||||
|
||||
REVISAO_CAMERA = (-0.25, -0.95, 0.15)
|
||||
|
||||
|
||||
def revisao(novo):
|
||||
# janela de sistema de 13 x 13 (x = c - 6, z = 6 - r): moldura, três pontos, barra e quatro linhas de conteúdo
|
||||
linhas = {4: 8, 6: 10, 8: 6, 10: 9}
|
||||
# três blocos das linhas, da esquerda para a direita, saltados para a frente e girados: (deslocamento, giro)
|
||||
fora = {
|
||||
(3, 6): ((-0.4, -2.4, 1.1), (0.6, 0.3, 0.7)),
|
||||
(5, 4): ((0.5, -2.0, -0.9), (-0.5, 0.4, -0.6)),
|
||||
(7, 10): ((-0.3, -2.6, 0.9), (0.4, -0.6, 0.5)),
|
||||
}
|
||||
soltos = []
|
||||
for c in range(13):
|
||||
for r in range(13):
|
||||
if c in (0, 12) or r in (0, 2, 12) or (r == 1 and c in (2, 4, 6)) or 2 <= c <= linhas.get(r, 0):
|
||||
obj = novo(Vector((c - 6, 0, 6 - r)), 1, 0)
|
||||
if (c, r) in fora:
|
||||
desloc, giro = fora[(c, r)]
|
||||
soltos.append((obj, obj.location.copy(), Vector(desloc), Vector(giro)))
|
||||
|
||||
# lupa: aro octogonal em volta de um vão de 3 x 3 e cabo na diagonal, para baixo e à direita
|
||||
lupa = []
|
||||
for dx in range(-2, 3):
|
||||
for dz in range(-2, 3):
|
||||
if 1.5 < math.hypot(dx, dz) <= 2.6:
|
||||
lupa.append((novo((0, 0, 0), 1, 1), Vector((dx, 0, dz))))
|
||||
for k in range(3):
|
||||
d = (3.0 + 0.95 * k) / math.sqrt(2)
|
||||
cabo = novo((0, 0, 0), 1, 1)
|
||||
cabo.rotation_euler = (0, math.pi / 4, 0)
|
||||
lupa.append((cabo, Vector((d, 0, -d))))
|
||||
lima = lupa[0][0].material_slots[0].material
|
||||
porcelana = soltos[0][0].material_slots[0].material
|
||||
|
||||
# a lupa anda 3,5 à frente da janela, na linha de visão de cada bloco solto: o bloco aparece no meio do aro
|
||||
rumo = Vector(REVISAO_CAMERA).normalized()
|
||||
|
||||
def na_frente(p):
|
||||
return Vector(p) + rumo * (3.5 / -rumo.y)
|
||||
|
||||
a, b, c = (alvo for _, alvo, _, _ in soltos)
|
||||
caminho = [(0, na_frente((-18, 0, 1))), (9, na_frente(a)), (13, na_frente(a)), (21, na_frente(b)),
|
||||
(25, na_frente(b)), (33, na_frente(c)), (37, na_frente(c)), (47, na_frente((28, 0, -3)))] # fmt: skip
|
||||
chegadas = (9, 21, 33)
|
||||
selo = selo_conferido(novo, 2.3, -3.2, -1, 0.6) # menor, por dentro do canto de baixo à direita
|
||||
de_fora = Vector((0, 0, -11)) # chega e sai reto por baixo, sem invadir a direita do quadro
|
||||
|
||||
def lupa_em(f):
|
||||
for (f0, p0), (f1, p1) in zip(caminho, caminho[1:]):
|
||||
if f <= f1:
|
||||
t = min(max((f - f0) / (f1 - f0), 0), 1)
|
||||
return p0.lerp(p1, t * t * (3 - 2 * t))
|
||||
return caminho[-1][1]
|
||||
|
||||
def pose(f):
|
||||
centro = lupa_em(f)
|
||||
for obj, desvio in lupa:
|
||||
obj.location = centro + desvio
|
||||
for i, ((obj, alvo, desloc, giro), chega) in enumerate(zip(soltos, chegadas)):
|
||||
s = encaixe(f, chega, 6) - encaixe(f, 62 + 2 * i, 5) # no fim voltam a saltar, para o loop emendar
|
||||
obj.location = alvo + desloc * (1 - s)
|
||||
obj.rotation_euler = giro * (1 - s)
|
||||
obj.material_slots[0].material = lima if chega <= f < chega + 10 else porcelana
|
||||
d = de_fora * (1 - encaixe(f, 44, 8) + min(max((f - 60) / 6, 0), 1) ** 3)
|
||||
for obj, alvo in selo:
|
||||
obj.location = alvo + d
|
||||
|
||||
return pose
|
||||
|
||||
|
||||
# nome, peça, direção da câmera, distância, alvo, quadro do pôster (peça montada)
|
||||
PECAS = [
|
||||
("sistema", sistema, (-0.5, -0.8, 0.6), 44, (0, 0.5, 2.3), 36),
|
||||
("whatsapp", whatsapp, (-0.3, -0.93, 0.18), 64, (-3.8, 0, 0), 56),
|
||||
("404", quatrocentos, (-0.42, -0.86, 0.2), 46, (0, 0, 0), 0),
|
||||
("fiscal", fiscal, (-0.42, -0.86, 0.2), 72, (0, 0, 0), 36),
|
||||
("revisao", revisao, REVISAO_CAMERA, 68, (6.3, 0, -0.6), 20),
|
||||
]
|
||||
|
||||
|
||||
# ------------------------------------------------------------- ffmpeg
|
||||
|
||||
|
||||
def ffmpeg(*args):
|
||||
fundo = ["-f", "lavfi", "-i", f"color=c={TINTA}:s={LADO[0]}x{LADO[1]}:r=24"]
|
||||
subprocess.run(["ffmpeg", "-y", "-loglevel", "error", *fundo, *args], check=True)
|
||||
|
||||
|
||||
def codificar(pasta, nome, poster):
|
||||
destino = os.path.join(PUBLICO, f"peca-{nome}")
|
||||
# quadros com alfa sobre a tinta opaca, em yuv420p BT.709 (o padrão dos navegadores)
|
||||
compor = "[0][1]overlay=shortest=1:format=rgb,scale=out_color_matrix=bt709:out_range=tv,format=yuv420p"
|
||||
entrada = ["-framerate", "24", "-i", os.path.join(pasta, "%04d.png"), "-filter_complex", compor, "-an"]
|
||||
cor = ["-colorspace", "bt709", "-color_primaries", "bt709", "-color_trc", "bt709"]
|
||||
ffmpeg(*entrada, *cor, "-c:v", "libvpx-vp9", "-crf", "30", "-b:v", "0", "-row-mt", "1", "-cpu-used", "1",
|
||||
f"{destino}.webm") # fmt: skip
|
||||
ffmpeg(*entrada, *cor, "-c:v", "libx264", "-preset", "slow", "-crf", "22", "-movflags", "+faststart",
|
||||
f"{destino}.mp4") # fmt: skip
|
||||
ffmpeg("-i", poster, "-filter_complex", "[0][1]overlay", "-frames:v", "1", "-c:v", "libwebp", "-quality", "80",
|
||||
f"{destino}.webp") # fmt: skip
|
||||
for ext, limite in (("webm", LIMITE_VIDEO), ("mp4", LIMITE_VIDEO), ("webp", LIMITE_POSTER)):
|
||||
tamanho = os.path.getsize(f"{destino}.{ext}")
|
||||
print(f"peca-{nome}.{ext}: {tamanho} bytes", flush=True)
|
||||
if tamanho > limite:
|
||||
falhar(f"peca-{nome}.{ext} tem {tamanho} bytes, limite {limite}")
|
||||
|
||||
|
||||
# ------------------------------------------------------------- Blender
|
||||
|
||||
|
||||
def main():
|
||||
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||||
cena = bpy.context.scene
|
||||
cena.view_settings.view_transform = "Standard"
|
||||
cena.world = bpy.data.worlds.new("fundo")
|
||||
cena.render.use_persistent_data = True
|
||||
ligar_cycles(cena)
|
||||
luz_de_estudio(cena)
|
||||
|
||||
bloco = criar_bloco()
|
||||
cena.collection.objects.unlink(bloco) # a malha fica só nas instâncias
|
||||
malha = bloco.data
|
||||
bpy.data.objects.remove(bloco)
|
||||
materiais = criar_materiais()
|
||||
malha.materials.append(materiais[0])
|
||||
|
||||
camera = bpy.data.objects.new("camera", bpy.data.cameras.new("camera"))
|
||||
cena.collection.objects.link(camera)
|
||||
camera.data.lens = 85
|
||||
cena.camera = camera
|
||||
|
||||
colecoes = {}
|
||||
for nome, peca, direcao, distancia, alvo, quadro_poster in PECAS:
|
||||
col = bpy.data.collections.new(nome)
|
||||
cena.collection.children.link(col)
|
||||
colecoes[nome] = col
|
||||
|
||||
def novo(posicao, escala, tom, col=col):
|
||||
obj = bpy.data.objects.new(f"{col.name}.{len(col.objects):03d}", malha)
|
||||
col.objects.link(obj)
|
||||
obj.location = posicao
|
||||
obj.scale = escala if isinstance(escala, tuple) else (escala,) * 3
|
||||
obj.material_slots[0].link = "OBJECT"
|
||||
obj.material_slots[0].material = materiais[tom]
|
||||
return obj
|
||||
|
||||
pose = peca(novo)
|
||||
for outro, c in colecoes.items():
|
||||
c.hide_render = outro != nome
|
||||
direcao = Vector(direcao).normalized()
|
||||
camera.location = Vector(alvo) + direcao * distancia
|
||||
camera.rotation_euler = (-direcao).to_track_quat("-Z", "Y").to_euler()
|
||||
|
||||
pasta = os.path.join(SAIDA, "pecas", nome)
|
||||
os.makedirs(pasta, exist_ok=True)
|
||||
for f in range(QUADROS):
|
||||
pose(f)
|
||||
sair(cena, os.path.join(pasta, f"{f:04d}.png"), LADO, "PNG", "RGBA", AMOSTRAS, True)
|
||||
codificar(pasta, nome, os.path.join(pasta, f"{quadro_poster:04d}.png"))
|
||||
|
||||
ctypes.CDLL(None).fflush(None) # despeja o buffer do C (banner do Blender) antes do OK
|
||||
print(f"OK pecas={len(PECAS)}", flush=True)
|
||||
sys.exit(0) # sai já, para o OK ser a última linha
|
||||
|
||||
|
||||
main()
|
||||
Reference in new issue
Block a user