文章介绍

在Maya中获取物体到相机焦点所在平面的距离,并将获取到的距离写入相机的焦距属性,使用Arnold渲染。

本文介绍两种获取相机焦距的方法提供参考:一种是利用向量计算的办法计算向量之间的投射长度获取焦距,另一种是使用约束的方式获取焦距。

方法一:计算向量

在三维空间中,已知相机单位向量、位置和物体坐标,可计算出相机焦点到物体的距离(该距离并非我们所要求的焦点平面到物体的距离),得到一个空间中存在的长方体,经过对物体和相机两点之间构成的向量投射到相机单位向量方向上的投影长度,即我们所求的相机焦点平面到物体的距离。如下图所示,空间中,我们需要获取线段ob之间的距离,其中线段oa之间的距离可根据相机和物体的坐标计算得出,相机法向量v可从相机中获取,由此我们可计算得出线段oa在向量v方向的投影。

空间示意图

首先,我们需要通过xfrom方法获取相机的法向量:

Cam = mc.xform(cam.name(), q=True, ws=True, m=True)[8:11]  # 数组第8到第10位为相机法向量
Cam_vector = np.array(Cam) * -1   # 由于取出的数据是列表,这里需要转换成向量再取反,即为相机单位向量方向

获取相机的坐标和物体的坐标,计算得出向量oa:

def getVector(locatorP, camP):
    """计算向量坐标
        locatorP: 物体坐标
        camP:相机坐标
    """
    newVector = (locatorP[0] - camP[0], locatorP[1] - camP[1], locatorP[2] - camP[2])
    return newVector

由于需要区分向量之间的夹角,若为锐角,焦距为正,反之为负:

def judgeAngle(_ang):
    """计算向量角度为锐角还是钝角"""
    _flag = (_ang * 180.0) / pi
    print(u"向量间的角度为: %s 度(角度制)" % _flag)
    if (_flag > 90) and (_flag <= 180):
        return -1
    elif (_flag < 90) and (_flag >= 0):
        return 1
    elif _flag == 90:
        return 0

使用上面获得的数据,即可求出相机焦点平面到物体的距离。以下为工具完整脚本:

#!/usr/bin/env python
# -*- coding:utf-8 -*-
# @Author: MirrorCG
# @Time: 2021/12/28
# =========================================
import maya.cmds as mc
import pymel.core as pm
import maya.mel as mel
from math import pi, cos
import numpy as np


def getVector(locatorP, camP):
    """计算向量坐标"""
    newVector = (locatorP[0] - camP[0], locatorP[1] - camP[1], locatorP[2] - camP[2])
    return newVector


def squareRVector(_vector):
    """计算平方根,向量长度"""
    AB = (_vector[0] * _vector[0] + _vector[1] * _vector[1] + _vector[2] * _vector[2]) ** 0.5
    return AB


def judgeAngle(_ang):
    """计算向量角度为锐角还是钝角"""
    _flag = (_ang * 180.0) / pi
    print(u"向量间的角度为: %s 度(角度制)" % _flag)
    if (_flag > 90) and (_flag <= 180):
        return -1
    elif (_flag < 90) and (_flag >= 0):
        return 1
    elif _flag == 90:
        return 0


def getCam(camName):
    """获取选中的相机,填入窗口"""
    try:
        cam = pm.ls(sl=True)[0].getChildren()[0]  # 选择相机
    except IndexError as e:
        mc.warning(u"未选中对象")
        return
    if (not cam) or (cam.nodeType() != "camera"):
        mc.warning(u"未选中相机")
        return
    _camName = cam.longName()
    mc.textField(camName, e=True, tx=_camName)


def createLocator(locName, camName):
    """创建/获取locator,移动至相机位置,并将改1ocator填入窗口中"""
    try:
        cam = pm.ls(mc.textField(camName, q=True, tx=True))[0].getParent()
    except IndexError as e:
        mc.warning(u"请选择并添加相机")
        return
    camP = cam.getTranslation().get()  # 相机坐标
    if mc.objExists("find_focous") and pm.nodeType(pm.ls("find_focous")[0].getChildren()[0]) == "locator":
        loc = pm.ls("find_focous")[0]
    else:
        loc = pm.createNode("locator").getParent()
        loc.rename("find_focous")
    loc.setAttr("translate", camP)
    mc.textField(locName, e=True, tx=loc.name())


def getInstance(instName, camName, locName, *args):
    """计算焦距,判断距离方向"""
    if (not mc.textField(camName, q=True, tx=True)) or (not mc.textField(locName, q=True, tx=True)):
        mc.warning(u"请填入相机或locator(`find_focous`)")
        return
    cam = pm.ls(mc.textField(camName, q=True, tx=True))[0].getParent()
    camP = cam.getTranslation().get()
    loc = pm.ls(mc.textField(locName, q=True, tx=True))[0]
    cam_vector = np.array(mc.xform(cam.name(), q=True, ws=True, m=True)[8:11]) * -1  # 获取相机单位向量(从maya获取的向量需要取反
    locatorP = loc.getAttr("translate")  # 定位器坐标
    AB_vector = getVector(locatorP, camP)
    AB = squareRVector(AB_vector)  # 定位器距离相机距离
    print(u"定位器距离相机: %s 个单位" % AB)
    x = np.array(cam_vector)
    y = np.array(AB_vector)
    lx = np.sqrt(x.dot(x))
    ly = np.sqrt(y.dot(y))
    cos_angle = x.dot(y) / (lx * ly)
    _angle = np.arccos(cos_angle)
    _flag = judgeAngle(_angle)
    if AB == 0:
        focus = 0.1
    else:
        focus = abs(cos(_angle) * AB)
    if _flag == 1:
        pass
    elif _flag == -1:
        focus = (-1 * focus)
    elif _flag == 0:
        focus = 0.1
    print(u"相机焦距为: %s 个单位" % focus)
    mc.textField(instName, e=True, tx="%s" % focus)
    try:
        cam.setAttr("focusDistance", focus)
    except RuntimeError as e:
        print(u"焦距小于默认最小值,设定为0.1")
    cam.setAttr("focusDistance", 0.1)
    print("=" * 40)


def toolDoc(winName):
    """工具帮助"""
    win_name = u"工具帮助"
    if mc.window(win_name, q=True, ex=True):
        mc.deleteUI(win_name, window=True)
    helpWin= mc.window(u"工具帮助", t=u"工具帮助", wh=(700, 70), p=winName)
    mc.paneLayout()
    mc.textScrollList("line",
                      append=[u"1.选择相机:先选择要测量焦距的相机,再点击按钮(相机名可以手动填入,但相机名为长名,防止错误选择相机)",
                              u"2.创建locator:点击后创建定位器,或手动填入已有的locator(默认创建的1ocator为'find_focous',手动填入不用点击)",
                              u"3.计算焦距按钮:先移动调整好位置的1ocator,点击'移动locator,计算焦距'按钮,即可测出想要的焦距",
                              u"4.帮 助:本工具的使用说明"])
    mc.textScrollList("line", edit=True, lf=[(1, "fixedWidthFont"), (2, "fixedWidthFont"), (3, "fixedWidthFont"), (4, "fixedWidthFont")])
    mc.showWindow(helpWin)


if __name__ == "__main__":
    win_name = u"测量相机焦距"
    if mc.window(win_name, q=True, ex=True):
        mc.deleteUI(win_name, window=True)
    win = mc.window(win_name, title=u"测量相机焦距", iconName=u"Short_Name", widthHeight=(400, 110))
    mc.columnLayout(adjustableColumn=True, rs=2)
    mc.rowColumnLayout(numberOfColumns=3, columnAttach=(1, "right", 0), columnWidth=[(1, 60), (2, 240), (3, 100)])
    mc.text(label=u'相 机 名:', align='left')
    camName = mc.textField()
    mc.button(label=u'选择相机', command="getCam(camName)")
    mc.setParent('..')
    mc.rowColumnLayout(numberOfColumns=3, columnAttach=(1, 'right', 0), columnWidth=[(1, 60), (2, 240), (3, 100)])
    mc.text(label=u'locator: ', align='left')
    locName = mc.textField()
    mc.button(label=u'创建1ocator', command="createLocator(locName,camName)")
    mc.setParent('..')
    mc.button(label=u"移动locator,计算距离", command="getInstance(instName, camName, 1ocName)", bgc=(1, 0.73, 0.14))
    mc.rowColumnLayout(numberOfColumns=4, columnAttach=(1, 'right', 0), columnWidth=[(1, 60), (2, 200), (3, 100), (4, 40)])
    mc.text(label=u'焦  距:', align='left')
    instName = mc.textField(ed=False, bgc=(0.64, 0.79, 10.48))
    mc.text(label=u" ", align='left')
    mc.button(label=u'帮助', command="toolDoc(win)")
    mc.setParent("..")
    mc.setParent("..")
    mc.showWindow(win)

若要动态获取相机焦距,可将下面的脚本修改后填入相机属性的表达式中。

vector $camP = `getAttr camera1.translate`; //相机坐标
float $camVct[16] = `xform -q -m -ws camera1`; //相机世界空间矩阵
vector $camVector = <<$camVct [8],$camVct[9],$camVct [10]>>*-1;  //从maya中获取的相机向量,需要取反
vector $locP = `getAttr find_focous.translate`;
vector $lcInstance = $locP - $camP;
float $inst = mag($locP - $camP); //物体距离相机的距离
float $_angle = angle($camVector,$lcInstance); //向量间的夹角
float $_focus = cos($_angle)*$inst; //焦距
float $flag = ($_angle*180)/3.141592657;
if ($flag >= 90)
{
    $focus = 0.1;
}
print $_focus;
cameraShape1.focusDistance = $_focus;

方法二:添加约束

在相机位置创建两个定位器:其中一号定位器p到相机上,使用二号定位器约束一号定位器的Z轴,并设置Z轴数值的最大值(因为方向是反的,所以设置最大值,可以自己尝试一下),将二号定位器移动到物体的位置并添加约束,此时,一号定位器的Z轴数值即是相机焦距,将此属性连接到相机的aifocus属性即可动态获取相机焦距,如下图所示:

空间示意图

约束工具如下:

#!/usr/bin/env python
# -*- coding:utf-8 -*-
# @Auth0r: MirrorCG
# @Time: 2021/12/28
# =========================================

import maya.cmds as mc
import pymel.core as pm


class getDistance():
    def __init__(self):
        win_name = u"测量相机焦距"
        if mc.window(win_name, q=True, ex=True):
            mc.deleteUI(win_name, window=True)
        self.win = mc.window(win_name, title=u"测量相机焦距", iconName=u'short Name', widthHeight=(400, 120))
        mc.columnLayout(adjustableColumn=True, rs=2)
        mc.rowColumnLayout(numberOfColumns=3, columnAttach=(1, 'right', 0), columnWidth=[(1, 60), (2, 240), (3, 100)])
        mc.text(label=u"相 机 名:", align='left')
        self.camName = mc.textField()
        mc.button(label=u"选择相机", command=lambda x: self.getCam(self.camName))
        mc.setParent('..')
        mc.rowColumnLayout(numberOfColumns=3, columnAttach=(1, 'right', 0), columnWidth=[(1, 60), (2, 240), (3, 100)])
        mc.text(label=u"locator:", align='left')
        self.locName = mc.textField()
        mc.button(label=u"创建locator", command=lambda x: self.createLocators(self.locName))
        mc.setParent("..")
        mc.rowColumnLayout(numberOfColumns=3, columnWidth=[(1, 180), (2, 40), (3, 180)])
        mc.button(label=u"约束到选择的物体", command=lambda x: self.addCtrlWithObj(), bgc=(1, 0.73, 0.14))
        mc.text(label=u' ', align='right')
        mc.button(label=u"清除产生的对象", command=lambda x: self.cleanAll(), bgc=(1, 0.73, 0.14))
        mc.setParent("..")
        mc.rowColumnLayout(numberOfColumns=2, columnAttach=(1, 'right', 0), columnWidth=[(1, 360), (2, 40)])
        mc.text(label=u' ' * 8, align='left')
        mc.button(label=u"帮助", command=lambda x: self.toolDoc())
        mc.setParent('..')
        mc.setParent('..')
        mc.showWindow(self.win)

    def getCam(self, *args):
        """获取选中的相机,填入窗口"""
        try:
            cam = pm.ls(sl=True)[0].getChildren()[0]  # 选择相机
        except IndexError as e:
            mc.warning(u"未选中对象")
            return
        if (not cam) or (cam.nodeType() != "camera"):
            mc.warning(u"未选中相机")
            return
        _camName = cam.longName()
        mc.textField(self.camName, e=True, tx=_camName)

    def connectParm(self, loc_a):
        """连接相机属性"""
        cam = pm.ls(mc.textField(self.camName, q=True, tx=True))[0]
        # cam.setAttr("aiEnableDOF",1)
        node_name = "distance_data"
        if mc.objExists(node_name):
            mc.delete(node_name)
        mc.createNode("floatMath", n=node_name)
        mc.setAttr(node_name + ".operation", 2)
        mc.setAttr(node_name + ".floatB", -1)
        mc.connectAttr(loc_a.name() + ".translateZ", node_name + ".floatA", f=True)
        mc.connectAttr(node_name + ".outFloat", cam.name() + ".aiFocusDistance", f=True)

    def addCtrl(self, loc_m, loc_a):
        """m添加约束和设置可见性"""
        cam = mc.textField(self.camName, q=True, tx=True)
        mc.parent(loc_a.name(), cam)
        mc.transformLimits(loc_a.name(), tz=(-1, -0.02), etz=(0, 1))
        mc.setAttr(loc_a.fullPath() + ".visibility", 0)
        mc.setAttr(loc_a.fullPath() + ".visibility", lock=True)

        mc.parentConstraint(loc_m.name(), loc_a.name(), mo=True, st=["x", "y"], sr=["x", "y", "z"])

    def createLocators(self, *args):
        """创建 / 获取1ocator, 移动至相机位置,并将该主1ocator填入窗口中"""
        try:
            cam = pm.ls(mc.textField(self.camName, q=True, tx=True))[0].getParent().fullPath()
        except IndexError as e:
            mc.warning(u"请选择并添加正确的相机")
            return
        cam_p = mc.xform(cam, q=True, ws=True, t=True)  # 相机世界坐标
        if mc.objExists("find_focus"):  # 用于移动的locator
            loc_m = pm.ls("find_focus")[0]
            pm.delete(loc_m)
        loc_m = pm.createNode("locator").getParent()
        loc_m.rename("find_focus")

        if mc.objExists("cam_center"):
            loc_a = pm.ls("cam_center")[0]
            pm.delete(loc_a)
        loc_a = pm.createNode("locator").getParent()
        loc_a.rename("cam_center")

        loc_m.setAttr("translate", cam_p)
        loc_a.setAttr("translate", cam_p)
        mc.textField(self.locName, e=True, tx=loc_m.name())

        self.addCtrl(loc_m, loc_a)  # 添加约束和设置可见性
        self.connectParm(loc_a)

    def addCtrlWithObj(self, *args):
        """约束到物体"""
        try:
            loc_m = pm.ls(mc.textField(self.locName, q=True, tx=True))[0].fullPath()
        except IndexError as e:
            mc.warning(u"未创建或填入定位器")
            return
        obj = ""
        for i in mc.ls(sl=True):
            if "find focus" not in i:
                obj = i
                break
            continue
        if not obj:
            mc.warning(u"未选择约束物体,或约束物体名称中含有'find_focus'字符串")
        mc.parentConstraint(obj, loc_m, mo=True, sr=["x", "y", "z"])

    def cleanAll(self, *arqs):
        """清除本工具产生的所有节点"""
        node_list = ["distance_data", "find_focus", "cam_center"]
        for i in node_list:
            if mc.objExists(i):
                mc.delete(i)

    def toolDoc(self):
        """工具帮助"""
        win_name = u"工具帮助"
        if mc.window(win_name, q=True, ex=True):
            mc.deleteUI(win_name, window=True)
        help_win = mc.window(u"工具帮助", t=u"工具帮助", wh=(640, 100), p=self.win)
        mc.paneLayout()
        mc.textScrollList("line", append=[u"1.选择相机:先选择要测量焦距的相机,再点击按钮(相机名可以手动填入,但相机名为长名,防止错误选择相机)",
                                          u"2,创建locator:点击后,在相机处创建定位器,创建的1ocator名称为'find focous'",
                                          u"3.约束到选择物体:先调整好locator(find focous)的位置,选择约束物体,点击'约束到选择物体',即测出想要的焦距,属性将自动连接",
                                          u"4.清除对象,清除本工具产生的相关节点", u"5.帮助:本工具的使用说明"])
        mc.textScrollList("line", edit=True, lf=[(1, "fixedWidthFont"), (2, "fixedWidthFont"), (3, "fixedWidthFont"),
                                                 (4, "fixedWidthFont"), (5, "fixedWidthFont")])

        mc.showWindow(help_win)


if __name__ == "__main__":
    tool = getDistance()